summaryrefslogtreecommitdiffstats
path: root/ansible_collections/netapp/ontap/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-18 05:52:35 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-18 05:52:35 +0000
commit7fec0b69a082aaeec72fee0612766aa42f6b1b4d (patch)
treeefb569b86ca4da888717f5433e757145fa322e08 /ansible_collections/netapp/ontap/plugins
parentReleasing progress-linux version 7.7.0+dfsg-3~progress7.99u1. (diff)
downloadansible-7fec0b69a082aaeec72fee0612766aa42f6b1b4d.tar.xz
ansible-7fec0b69a082aaeec72fee0612766aa42f6b1b4d.zip
Merging upstream version 9.4.0+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/netapp/ontap/plugins')
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/netapp.py2
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_broadcast_domain.py1
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_cg_snapshot.py256
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_server.py85
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_unix_symlink_mapping.py289
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_cli_timeout.py123
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster.py76
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster_peer.py70
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_dns.py10
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_config.py186
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_destination.py191
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_filter.py121
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_export_policy_rule.py6
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_file_security_permissions_acl.py5
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_igroup_initiator.py9
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_info.py2
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_kerberos_realm.py65
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_login_messages.py5
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun.py21
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map.py16
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map_reporting_nodes.py22
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_name_mappings.py6
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_net_ifgrp.py13
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_nfs.py5
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_node.py15
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_qos_policy_group.py46
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_rest_info.py17
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_restit.py6
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_s3_services.py54
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_security_certificates.py10
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_service_policy.py7
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapmirror.py16
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapshot_policy.py11
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp.py171
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp_config.py142
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_storage_auto_giveback.py11
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_svm.py8
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_user.py3
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_volume.py180
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_vscan_scanner_pool.py209
-rw-r--r--ansible_collections/netapp/ontap/plugins/modules/na_ontap_vserver_peer.py19
41 files changed, 2130 insertions, 380 deletions
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/netapp.py b/ansible_collections/netapp/ontap/plugins/module_utils/netapp.py
index 28d9428a2..f41139423 100644
--- a/ansible_collections/netapp/ontap/plugins/module_utils/netapp.py
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/netapp.py
@@ -48,7 +48,7 @@ try:
except ImportError:
ANSIBLE_VERSION = 'unknown'
-COLLECTION_VERSION = "22.7.0"
+COLLECTION_VERSION = "22.10.0"
CLIENT_APP_VERSION = "%s/%s" % ("%s", COLLECTION_VERSION)
IMPORT_EXCEPTION = None
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_broadcast_domain.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_broadcast_domain.py
index ef74d1705..11c762d7c 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_broadcast_domain.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_broadcast_domain.py
@@ -47,6 +47,7 @@ options:
- Specify the required ipspace for the broadcast domain.
- With ZAPI, a domain ipspace cannot be modified after the domain has been created.
- With REST, a domain ipspace can be modified.
+ - This option is required while using REST.
type: str
from_ipspace:
description:
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cg_snapshot.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cg_snapshot.py
index 313bf223e..20bca605d 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cg_snapshot.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cg_snapshot.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2018-2019, NetApp, Inc
+# (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
@@ -14,17 +14,17 @@ DOCUMENTATION = '''
short_description: NetApp ONTAP manage consistency group snapshot
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
description:
- - Create consistency group snapshot for ONTAP volumes.
- - This module only supports ZAPI and is deprecated.
- - The final version of ONTAP to support ZAPI is 9.12.1.
+ - Create or delete consistency group snapshot for ONTAP volumes.
extends_documentation_fragment:
- - netapp.ontap.netapp.na_ontap_zapi
+ - netapp.ontap.netapp.na_ontap
module: na_ontap_cg_snapshot
options:
state:
description:
- - If you want to create a snapshot.
+ - Specifies whether to create or delete the snapshot.
+ - Choice 'absent' is valid only with REST.
default: present
+ choices: ['present', 'absent']
type: str
vserver:
required: true
@@ -32,11 +32,19 @@ options:
description:
- Name of the vserver.
volumes:
- required: true
+ required: false
type: list
elements: str
description:
- A list of volumes in this filer that is part of this CG operation.
+ - Required with ZAPI.
+ consistency_group:
+ required: false
+ type: str
+ description:
+ - Name of the consistency group for which snapshot needs to be created or deleted.
+ - Valid only with REST.
+ version_added: 22.8.0
snapshot:
required: true
type: str
@@ -45,6 +53,7 @@ options:
timeout:
description:
- Timeout selector.
+ - Not supported with REST.
choices: ['urgent', 'medium', 'relaxed']
type: str
default: medium
@@ -52,8 +61,18 @@ options:
description:
- A human readable SnapMirror label to be attached with the consistency group snapshot copies.
type: str
+ comment:
+ description:
+ - Comment for the snapshot copy.
+ - Only supported with REST.
+ type: str
+ version_added: 22.8.0
version_added: 2.7.0
+notes:
+ - REST support requires ONTAP 9.10 or later.
+ - Delete operation is supported only with REST.
+
'''
EXAMPLES = """
@@ -66,6 +85,40 @@ EXAMPLES = """
username: "{{ netapp username }}"
password: "{{ netapp password }}"
hostname: "{{ netapp hostname }}"
+
+ - name: Create CG snapshot using CG name - REST
+ na_ontap_cg_snapshot:
+ state: present
+ vserver: vserver_name
+ snapshot: snapshot_name
+ consistency_group: cg_name
+ snapmirror_label: sm_label
+ username: "{{ netapp username }}"
+ password: "{{ netapp password }}"
+ hostname: "{{ netapp hostname }}"
+
+ - name: Create CG snapshot using volumes - REST
+ na_ontap_cg_snapshot:
+ state: present
+ vserver: vserver_name
+ snapshot: snapshot_name
+ volumes:
+ - vol1
+ - vol2
+ snapmirror_label: sm_label
+ username: "{{ netapp username }}"
+ password: "{{ netapp password }}"
+ hostname: "{{ netapp hostname }}"
+
+ - name: Delete CG snapshot - REST
+ na_ontap_cg_snapshot:
+ state: absent
+ vserver: vserver_name
+ snapshot: snapshot_name
+ consistency_group: cg_name
+ username: "{{ netapp username }}"
+ password: "{{ netapp password }}"
+ hostname: "{{ netapp hostname }}"
"""
RETURN = """
@@ -77,55 +130,56 @@ from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule
-
-HAS_NETAPP_LIB = netapp_utils.has_netapp_lib()
+from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic
class NetAppONTAPCGSnapshot(object):
"""
- Methods to create CG snapshots
+ Methods to create or delete CG snapshots
"""
def __init__(self):
- self.argument_spec = netapp_utils.na_ontap_zapi_only_spec()
+ self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
self.argument_spec.update(dict(
- state=dict(required=False, type='str', default='present'),
+ state=dict(required=False, choices=['present', 'absent'], default='present'),
vserver=dict(required=True, type='str'),
- volumes=dict(required=True, type='list', elements='str'),
+ volumes=dict(required=False, type='list', elements='str'),
snapshot=dict(required=True, type='str'),
timeout=dict(required=False, type='str', choices=[
'urgent', 'medium', 'relaxed'], default='medium'),
- snapmirror_label=dict(required=False, type='str')
+ snapmirror_label=dict(required=False, type='str'),
+ consistency_group=dict(required=False, type='str'),
+ comment=dict(required=False, type='str'),
))
self.module = AnsibleModule(
argument_spec=self.argument_spec,
- supports_check_mode=False
+ supports_check_mode=False,
+ mutually_exclusive=[
+ ['consistency_group', 'volumes']]
)
- parameters = self.module.params
-
- # set up variables
- self.state = parameters['state']
- self.vserver = parameters['vserver']
- self.volumes = parameters['volumes']
- self.snapshot = parameters['snapshot']
- self.timeout = parameters['timeout']
- self.snapmirror_label = parameters['snapmirror_label']
- self.cgid = None
- NetAppModule().module_deprecated(self.module)
- if HAS_NETAPP_LIB is False:
- self.module.fail_json(
- msg="the python NetApp-Lib module is required")
+ self.na_helper = NetAppModule()
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ self.rest_api = netapp_utils.OntapRestAPI(self.module)
+ self.use_rest = self.rest_api.is_rest()
+
+ if self.use_rest:
+ if not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 10, 1):
+ self.module.fail_json(msg='REST requires ONTAP 9.10.1 or later for /application/consistency-groups APIs.')
+ self.cg_uuid = None
else:
- self.server = netapp_utils.setup_na_ontap_zapi(
- module=self.module, vserver=self.vserver)
+ self.cgid = None
+ if not netapp_utils.has_netapp_lib():
+ self.module.fail_json(msg=netapp_utils.netapp_lib_is_required())
+ self.zapi_errors()
+ self.server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.parameters['vserver'])
def does_snapshot_exist(self, volume):
"""
This is duplicated from na_ontap_snapshot
Checks to see if a snapshot exists or not
- :return: Return True if a snapshot exists, false if it dosn't
+ :return: Return True if a snapshot exists, false if it dosen't
"""
# TODO: Remove this method and import snapshot module and
# call get after re-factoring __init__ across all the modules
@@ -141,9 +195,9 @@ class NetAppONTAPCGSnapshot(object):
# compose query
query = netapp_utils.zapi.NaElement("query")
snapshot_info_obj = netapp_utils.zapi.NaElement("snapshot-info")
- snapshot_info_obj.add_new_child("name", self.snapshot)
+ snapshot_info_obj.add_new_child("name", self.parameters['snapshot'])
snapshot_info_obj.add_new_child("volume", volume)
- snapshot_info_obj.add_new_child("vserver", self.vserver)
+ snapshot_info_obj.add_new_child("vserver", self.parameters['vserver'])
query.add_child_elem(snapshot_info_obj)
snapshot_obj.add_child_elem(query)
result = self.server.invoke_successfully(snapshot_obj, True)
@@ -164,7 +218,7 @@ class NetAppONTAPCGSnapshot(object):
if self.cgid is not None:
self.cg_commit()
else:
- self.module.fail_json(msg="Error fetching CG ID for CG commit %s" % self.snapshot,
+ self.module.fail_json(msg="Error fetching CG ID for CG commit %s" % self.parameters['snapshot'],
exception=traceback.format_exc())
return started
@@ -174,19 +228,19 @@ class NetAppONTAPCGSnapshot(object):
"""
snapshot_started = False
cgstart = netapp_utils.zapi.NaElement("cg-start")
- cgstart.add_new_child("snapshot", self.snapshot)
- cgstart.add_new_child("timeout", self.timeout)
+ cgstart.add_new_child("snapshot", self.parameters['snapshot'])
+ cgstart.add_new_child("timeout", self.parameters['timeout'])
volume_list = netapp_utils.zapi.NaElement("volumes")
cgstart.add_child_elem(volume_list)
- for vol in self.volumes:
+ for vol in self.parameters['volumes']:
snapshot_exists = self.does_snapshot_exist(vol)
if snapshot_exists is None:
snapshot_started = True
volume_list.add_new_child("volume-name", vol)
if snapshot_started:
- if self.snapmirror_label:
+ if self.parameters.get('snapmirror_label') is not None:
cgstart.add_new_child("snapmirror-label",
- self.snapmirror_label)
+ self.parameters['snapmirror_label'])
try:
cgresult = self.server.invoke_successfully(
cgstart, enable_tunneling=True)
@@ -194,7 +248,7 @@ class NetAppONTAPCGSnapshot(object):
self.cgid = cgresult['cg-id']
except netapp_utils.zapi.NaApiError as error:
self.module.fail_json(msg="Error creating CG snapshot %s: %s" %
- (self.snapshot, to_native(error)),
+ (self.parameters['snapshot'], to_native(error)),
exception=traceback.format_exc())
return snapshot_started
@@ -209,18 +263,126 @@ class NetAppONTAPCGSnapshot(object):
enable_tunneling=True)
except netapp_utils.zapi.NaApiError as error:
self.module.fail_json(msg="Error committing CG snapshot %s: %s" %
- (self.snapshot, to_native(error)),
+ (self.parameters['snapshot'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def zapi_errors(self):
+ unsupported_zapi_properties = ['consistency_group', 'comment']
+ used_unsupported_zapi_properties = [option for option in unsupported_zapi_properties if option in self.parameters]
+ if used_unsupported_zapi_properties:
+ self.module.fail_json(msg="Error: %s options supported only with REST." % " ,".join(used_unsupported_zapi_properties))
+ if self.parameters.get('volumes') is None:
+ self.module.fail_json(msg="Error: 'volumes' option is mandatory while using ZAPI.")
+ if self.parameters.get('state') == 'absent':
+ self.module.fail_json(msg="Deletion of consistency group snapshot is not supported with ZAPI.")
+
+ def get_cg_rest(self):
+ """
+ Retrieve consistency group with the given CG name or list of volumes
+ """
+ api = '/application/consistency-groups'
+ query = {
+ 'svm.name': self.parameters['vserver'],
+ 'fields': 'svm.uuid,name,uuid,'
+ }
+
+ if self.parameters.get('consistency_group') is not None:
+ query['name'] = self.parameters['consistency_group']
+ record, error = rest_generic.get_one_record(self.rest_api, api, query)
+ if error:
+ self.module.fail_json(msg='Error searching for consistency group %s: %s' % (self.parameters['consistency_group'], to_native(error)),
+ exception=traceback.format_exc())
+ if record:
+ self.cg_uuid = record.get('uuid')
+
+ if self.parameters.get('volumes') is not None:
+ query['fields'] += 'volumes.name,'
+ records, error = rest_generic.get_0_or_more_records(self.rest_api, api, query)
+ if error:
+ self.module.fail_json(msg='Error searching for consistency group having volumes %s: %s' % (self.parameters['volumes'], to_native(error)),
+ exception=traceback.format_exc())
+ if records:
+ for record in records:
+ if record.get('volumes') is not None:
+ cg_volumes = [vol_item['name'] for vol_item in record['volumes']]
+ if cg_volumes == self.parameters['volumes']:
+ self.cg_uuid = record.get('uuid')
+ break
+ return None
+
+ def get_cg_snapshot_rest(self):
+ """
+ Retrieve CG snapshots using fetched CG uuid
+ """
+ self.get_cg_rest()
+ if self.cg_uuid is None:
+ if self.parameters.get('consistency_group') is not None:
+ self.module.fail_json(msg="Consistency group named '%s' not found" % self.parameters.get('consistency_group'))
+ if self.parameters.get('volumes') is not None:
+ self.module.fail_json(msg="Consistency group having volumes '%s' not found" % self.parameters.get('volumes'))
+
+ api = '/application/consistency-groups/%s/snapshots' % self.cg_uuid
+ query = {'name': self.parameters['snapshot'],
+ 'fields': 'name,'
+ 'uuid,'
+ 'consistency_group,'
+ 'snapmirror_label,'
+ 'comment,'}
+ record, error = rest_generic.get_one_record(self.rest_api, api, query)
+ if error:
+ self.module.fail_json(msg='Error searching for consistency group snapshot %s: %s' % (self.parameters['snapshot'], to_native(error)),
+ exception=traceback.format_exc())
+ if record:
+ return {
+ 'snapshot': record.get('name'),
+ 'snapshot_uuid': record.get('uuid'),
+ 'consistency_group': self.na_helper.safe_get(record, ['consistency_group', 'name']),
+ 'snapmirror_label': record.get('snapmirror_label'),
+ 'comment': record.get('comment'),
+ }
+ return None
+
+ def create_cg_snapshot_rest(self):
+ """Create CG snapshot"""
+ api = '/application/consistency-groups/%s/snapshots' % self.cg_uuid
+ body = {'name': self.parameters['snapshot']}
+ if self.parameters.get('snapmirror_label'):
+ body['snapmirror_label'] = self.parameters['snapmirror_label']
+ if self.parameters.get('comment'):
+ body['comment'] = self.parameters['comment']
+ dummy, error = rest_generic.post_async(self.rest_api, api, body)
+ if error:
+ self.module.fail_json(msg='Error creating consistency group snapshot %s: %s' % (self.parameters['snapshot'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def delete_cg_snapshot_rest(self, current):
+ """Delete CG snapshot"""
+ api = '/application/consistency-groups/%s/snapshots' % self.cg_uuid
+ dummy, error = rest_generic.delete_async(self.rest_api, api, current['snapshot_uuid'])
+ if error:
+ self.module.fail_json(msg='Error deleting consistency group snapshot %s: %s' % (self.parameters['snapshot'], to_native(error)),
exception=traceback.format_exc())
def apply(self):
- '''Applies action from playbook'''
- if not self.module.check_mode:
- changed = self.cgcreate()
- self.module.exit_json(changed=changed)
+ """Applies action from playbook"""
+ if not self.use_rest:
+ if not self.module.check_mode:
+ changed = self.cgcreate()
+ self.module.exit_json(changed=changed)
+ current = self.get_cg_snapshot_rest()
+ cd_action = self.na_helper.get_cd_action(current, self.parameters)
+
+ if self.na_helper.changed and not self.module.check_mode:
+ if cd_action == 'create':
+ self.create_cg_snapshot_rest()
+ elif cd_action == 'delete':
+ self.delete_cg_snapshot_rest(current)
+ result = netapp_utils.generate_result(self.na_helper.changed, cd_action)
+ self.module.exit_json(**result)
def main():
- '''Execute action from playbook'''
+ """Execute action from playbook"""
cg_obj = NetAppONTAPCGSnapshot()
cg_obj.apply()
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_server.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_server.py
index 8a65dd6c5..08a69c1f7 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_server.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_server.py
@@ -1,7 +1,7 @@
#!/usr/bin/python
""" this is cifs_server module
- (c) 2018-2022, NetApp, Inc
+ (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
@@ -70,6 +70,13 @@ options:
version_added: 2.7.0
type: str
+ default_site:
+ description:
+ - Specifies the site within the Active Directory domain to associate with the CIFS server if Data ONTAP cannot determine an appropriate site.
+ - Only supported with REST and requires ontap version 9.13.1 or later.
+ version_added: 22.8.0
+ type: str
+
force:
type: bool
description:
@@ -175,6 +182,21 @@ options:
type: str
version_added: 21.20.0
+ lm_compatibility_level:
+ description:
+ - Specifies CIFS server minimum security level, also known as the LMCompatibilityLevel.
+ - Only supported with REST and requires ontap version 9.8 or later. Use na_ontap_vserver_cifs_security with ZAPI.
+ choices: ['lm_ntlm_ntlmv2_krb', 'ntlm_ntlmv2_krb', 'ntlmv2_krb', 'krb']
+ type: str
+ version_added: 22.9.0
+
+ is_multichannel_enabled:
+ description:
+ - Specifies whether the CIFS server supports Multichannel or not.
+ - Only supported with REST and requires ontap version 9.10 or later.
+ type: bool
+ version_added: 22.10.0
+
'''
EXAMPLES = '''
@@ -239,16 +261,16 @@ EXAMPLES = '''
name: data2
vserver: svm1
service_state: stopped
- encrypt_dc_connection: True,
- smb_encryption: True,
- kdc_encryption: True,
- smb_signing: True,
- aes_netlogon_enabled: True,
- ldap_referral_enabled: True,
- session_security: seal,
- try_ldap_channel_binding: False,
- use_ldaps: True,
- use_start_tls": True
+ encrypt_dc_connection: True
+ smb_encryption: True
+ kdc_encryption: True
+ smb_signing: True
+ aes_netlogon_enabled: True
+ ldap_referral_enabled: True
+ session_security: seal
+ try_ldap_channel_binding: False
+ use_ldaps: True
+ use_start_tls: True
restrict_anonymous: no_access
domain: "{{ id_domain }}"
admin_user_name: "{{ domain_login }}"
@@ -289,6 +311,7 @@ class NetAppOntapcifsServer:
admin_user_name=dict(required=False, type='str'),
admin_password=dict(required=False, type='str', no_log=True),
ou=dict(required=False, type='str'),
+ default_site=dict(required=False, type='str'),
force=dict(required=False, type='bool'),
vserver=dict(required=True, type='str'),
from_name=dict(required=False, type='str'),
@@ -300,9 +323,11 @@ class NetAppOntapcifsServer:
aes_netlogon_enabled=dict(required=False, type='bool'),
ldap_referral_enabled=dict(required=False, type='bool'),
session_security=dict(required=False, type='str', choices=['none', 'sign', 'seal']),
+ lm_compatibility_level=dict(required=False, type='str', choices=['lm_ntlm_ntlmv2_krb', 'ntlm_ntlmv2_krb', 'ntlmv2_krb', 'krb']),
try_ldap_channel_binding=dict(required=False, type='bool'),
use_ldaps=dict(required=False, type='bool'),
- use_start_tls=dict(required=False, type='bool')
+ use_start_tls=dict(required=False, type='bool'),
+ is_multichannel_enabled=dict(required=False, type='bool'),
))
self.module = AnsibleModule(
@@ -318,15 +343,16 @@ class NetAppOntapcifsServer:
# Set up Rest API
self.rest_api = OntapRestAPI(self.module)
unsupported_rest_properties = ['workgroup']
- partially_supported_rest_properties = [['encrypt_dc_connection', (9, 8)], ['aes_netlogon_enabled', (9, 10, 1)], ['ldap_referral_enabled', (9, 10, 1)],
- ['session_security', (9, 10, 1)], ['try_ldap_channel_binding', (9, 10, 1)], ['use_ldaps', (9, 10, 1)],
- ['use_start_tls', (9, 10, 1)], ['force', (9, 11)]]
+ partially_supported_rest_properties = [['encrypt_dc_connection', (9, 8)], ['lm_compatibility_level', (9, 8)],
+ ['aes_netlogon_enabled', (9, 10, 1)], ['ldap_referral_enabled', (9, 10, 1)], ['session_security', (9, 10, 1)],
+ ['try_ldap_channel_binding', (9, 10, 1)], ['use_ldaps', (9, 10, 1)], ['use_start_tls', (9, 10, 1)],
+ ['is_multichannel_enabled', (9, 10, 1)], ['force', (9, 11)], ['default_site', (9, 13, 1)]]
self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, unsupported_rest_properties, partially_supported_rest_properties)
if not self.use_rest:
unsupported_zapi_properties = ['smb_signing', 'encrypt_dc_connection', 'kdc_encryption', 'smb_encryption', 'restrict_anonymous',
'aes_netlogon_enabled', 'ldap_referral_enabled', 'try_ldap_channel_binding', 'session_security',
- 'use_ldaps', 'use_start_tls', 'from_name']
+ 'lm_compatibility_level', 'use_ldaps', 'use_start_tls', 'from_name', 'default_site', 'is_multichannel_enabled']
used_unsupported_zapi_properties = [option for option in unsupported_zapi_properties if option in self.parameters]
if used_unsupported_zapi_properties:
self.module.fail_json(msg="Error: %s options supported only with REST." % " ,".join(used_unsupported_zapi_properties))
@@ -460,7 +486,9 @@ class NetAppOntapcifsServer:
query['name'] = from_name or self.parameters['cifs_server_name']
api = 'protocols/cifs/services'
if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 8):
- query['fields'] += 'security.encrypt_dc_connection,'
+ security_option_9_8 = ('security.encrypt_dc_connection,'
+ 'security.lm_compatibility_level,')
+ query['fields'] += security_option_9_8
if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 10, 1):
security_option_9_10 = ('security.use_ldaps,'
@@ -470,6 +498,10 @@ class NetAppOntapcifsServer:
'security.ldap_referral_enabled,'
'security.aes_netlogon_enabled,')
query['fields'] += security_option_9_10
+
+ if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 10, 1):
+ service_option_9_10 = ('options.multichannel,')
+ query['fields'] += service_option_9_10
record, error = rest_generic.get_one_record(self.rest_api, api, query)
if error:
self.module.fail_json(msg="Error on fetching cifs: %s" % error)
@@ -486,10 +518,12 @@ class NetAppOntapcifsServer:
'aes_netlogon_enabled': self.na_helper.safe_get(record, ['security', 'aes_netlogon_enabled']),
'ldap_referral_enabled': self.na_helper.safe_get(record, ['security', 'ldap_referral_enabled']),
'session_security': self.na_helper.safe_get(record, ['security', 'session_security']),
+ 'lm_compatibility_level': self.na_helper.safe_get(record, ['security', 'lm_compatibility_level']),
'try_ldap_channel_binding': self.na_helper.safe_get(record, ['security', 'try_ldap_channel_binding']),
'use_ldaps': self.na_helper.safe_get(record, ['security', 'use_ldaps']),
'use_start_tls': self.na_helper.safe_get(record, ['security', 'use_start_tls']),
- 'restrict_anonymous': self.na_helper.safe_get(record, ['security', 'restrict_anonymous'])
+ 'restrict_anonymous': self.na_helper.safe_get(record, ['security', 'restrict_anonymous']),
+ 'is_multichannel_enabled': self.na_helper.safe_get(record, ['options', 'multichannel']),
}
return record
@@ -503,17 +537,20 @@ class NetAppOntapcifsServer:
ad_domain['organizational_unit'] = self.parameters['ou']
if 'domain' in self.parameters:
ad_domain['fqdn'] = self.parameters['domain']
+ if 'default_site' in self.parameters:
+ ad_domain['default_site'] = self.parameters['default_site']
return ad_domain
def create_modify_body_rest(self, params=None):
"""
Function to define body for create and modify cifs server
"""
- body, query, security = {}, {}, {}
+ body, query, security, service_options = {}, {}, {}, {}
if params is None:
params = self.parameters
security_options = ['smb_signing', 'encrypt_dc_connection', 'kdc_encryption', 'smb_encryption', 'restrict_anonymous',
- 'aes_netlogon_enabled', 'ldap_referral_enabled', 'try_ldap_channel_binding', 'session_security', 'use_ldaps', 'use_start_tls']
+ 'aes_netlogon_enabled', 'ldap_referral_enabled', 'try_ldap_channel_binding', 'session_security',
+ 'lm_compatibility_level', 'use_ldaps', 'use_start_tls']
ad_domain = self.build_ad_domain()
if ad_domain:
body['ad_domain'] = ad_domain
@@ -524,6 +561,14 @@ class NetAppOntapcifsServer:
security[key] = params[key]
if security:
body['security'] = security
+ # for parameters having different key names in REST API and module inputs
+ for key, option in [
+ ('multichannel', 'is_multichannel_enabled'),
+ ]:
+ if option in params:
+ service_options.update({key: params[option]})
+ if service_options:
+ body['options'] = service_options
if 'vserver' in params:
body['svm.name'] = params['vserver']
if 'cifs_server_name' in params:
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_unix_symlink_mapping.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_unix_symlink_mapping.py
new file mode 100644
index 000000000..f8f1bfacb
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_unix_symlink_mapping.py
@@ -0,0 +1,289 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = """
+module: na_ontap_cifs_unix_symlink_mapping
+short_description: NetApp ONTAP module to manage UNIX symbolic link mapping for CIFS clients.
+extends_documentation_fragment:
+ - netapp.ontap.netapp.na_ontap
+version_added: '22.9.0'
+author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
+description:
+ - Create/ modify/ delete a UNIX symbolic link mapping for a CIFS client.
+options:
+ state:
+ description:
+ - Whether the specified symlink mapping should exist or not.
+ choices: ['present', 'absent']
+ type: str
+ default: present
+
+ vserver:
+ description:
+ - Name of the vserver to use.
+ type: str
+ required: true
+
+ unix_path:
+ description:
+ - Specifies the UNIX path prefix to be matched for the mapping.
+ - It must begin and end with a forward slash (/).
+ type: str
+ required: true
+
+ share_name:
+ description:
+ - Specifies the CIFS share name on the destination CIFS server to which the UNIX symbolic link is pointing.
+ type: str
+
+ cifs_server:
+ description:
+ - Specifies the destination CIFS server (DNS name, IP address, or NetBIOS name).
+ - This field is mandatory if the locality of the symbolic link is 'widelink'.
+ type: str
+
+ cifs_path:
+ description:
+ - Specifies the CIFS path on the destination to which the symbolic link maps.
+ - Note that this value is specified by using a UNIX-style path. It must begin and end with a forward slash (/).
+ type: str
+
+ locality:
+ description:
+ - Specifies whether the CIFS symbolic link is a local link or wide link. The default setting is local.
+ - The following values are supported
+ local - Local symbolic link maps only to the same CIFS share.
+ widelink - Wide symbolic link maps to any CIFS share on the network.
+ type: str
+ choices: ['local', 'widelink']
+ default: 'local'
+
+ home_directory:
+ description:
+ - Specify if the destination share is a home directory. The default value is false.
+ type: bool
+ default: False
+
+notes:
+ - Only supported with REST and requires ONTAP 9.6 or later.
+
+"""
+
+EXAMPLES = """
+ - name: Create a UNIX symlink mapping for CIFS share
+ netapp.ontap.na_ontap_cifs_unix_symlink_mapping:
+ state: present
+ vserver: "{{ svm }}"
+ unix_path: "/example1/"
+ share_name: "share1"
+ cifs_path: "/path1/test_dir/"
+ cifs_server: "CIFS"
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+ https: true
+ validate_certs: "{{ validate_certs }}"
+
+ - name: Update a specific UNIX symlink mapping for a SVM
+ netapp.ontap.na_ontap_cifs_unix_symlink_mapping:
+ state: present
+ vserver: "{{ svm }}"
+ unix_path: "/example1/"
+ share_name: "share2"
+ cifs_path: "/path2/test_dir/"
+ cifs_server: "CIFS"
+ locality: "widelink"
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+ https: true
+ validate_certs: "{{ validate_certs }}"
+
+ - name: Remove a specific UNIX symlink mapping for a SVM
+ netapp.ontap.na_ontap_cifs_unix_symlink_mapping:
+ state: absent
+ vserver: "{{ svm }}"
+ unix_path: "/example1/"
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+ https: true
+ validate_certs: "{{ validate_certs }}"
+
+"""
+
+RETURN = """
+"""
+
+import traceback
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_native
+import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
+from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic
+
+
+class NetAppOntapCifsUnixSymlink:
+ def __init__(self):
+ self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
+ self.argument_spec.update(dict(
+ state=dict(required=False, type='str', choices=['present', 'absent'], default='present'),
+ vserver=dict(required=True, type='str'),
+ unix_path=dict(required=True, type='str'),
+ share_name=dict(required=False, type='str'),
+ cifs_path=dict(required=False, type='str'),
+ cifs_server=dict(required=False, type='str'),
+ locality=dict(required=False, type='str', choices=['local', 'widelink'], default='local'),
+ home_directory=dict(required=False, type='bool', default=False)
+ ))
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ required_if=[
+ ('state', 'present', ['share_name', 'cifs_path']),
+ ('locality', 'widelink', ['cifs_server']),
+ ],
+ supports_check_mode=True
+ )
+ self.svm_uuid = None
+ self.na_helper = NetAppModule(self.module)
+ self.parameters = self.na_helper.check_and_set_parameters(self.module)
+ self.rest_api = netapp_utils.OntapRestAPI(self.module)
+ self.rest_api.fail_if_not_rest_minimum_version('na_ontap_cifs_unix_symlink_mapping:', 9, 6)
+
+ @staticmethod
+ def validate_path(path):
+ if not path.startswith('/'):
+ path = "/%s" % path
+ if not path.endswith('/'):
+ path = "%s/" % path
+ return path
+
+ @staticmethod
+ def encode_path(path):
+ return path.replace('/', '%2F')
+
+ def get_symlink_mapping_rest(self):
+ """
+ Retrieves a specific UNIX symbolink mapping for a SVM
+ """
+ api = 'protocols/cifs/unix-symlink-mapping'
+ query = {'svm.name': self.parameters.get('vserver'),
+ 'unix_path': self.parameters['unix_path'],
+ 'fields': 'svm.uuid,'
+ 'unix_path,'
+ 'target.share,'
+ 'target.path,'}
+ if self.parameters.get('cifs_server') is not None:
+ query['fields'] += 'target.server,'
+ if self.parameters.get('locality') is not None:
+ query['fields'] += 'target.locality,'
+ if self.parameters.get('home_directory') is not None:
+ query['fields'] += 'target.home_directory,'
+
+ record, error = rest_generic.get_one_record(self.rest_api, api, query)
+ if error:
+ self.module.fail_json(msg='Error while fetching cifs unix symlink mapping: %s' % to_native(error),
+ exception=traceback.format_exc())
+ if record:
+ self.svm_uuid = self.na_helper.safe_get(record, ['svm', 'uuid'])
+ return self.format_record(record)
+ return None
+
+ def format_record(self, record):
+ return {
+ 'unix_path': record.get('unix_path'),
+ 'share_name': self.na_helper.safe_get(record, ['target', 'share']),
+ 'cifs_path': self.na_helper.safe_get(record, ['target', 'path']),
+ 'cifs_server': self.na_helper.safe_get(record, ['target', 'server']),
+ 'locality': self.na_helper.safe_get(record, ['target', 'locality']),
+ 'home_directory': self.na_helper.safe_get(record, ['target', 'home_directory'])
+ }
+
+ def create_symlink_mapping_rest(self):
+ """
+ Creates a UNIX symbolink mapping for CIFS share
+ """
+ api = 'protocols/cifs/unix-symlink-mapping'
+ body = {
+ 'svm.name': self.parameters['vserver'],
+ 'unix_path': self.parameters['unix_path'],
+ 'target': {
+ 'share': self.parameters['share_name'],
+ 'path': self.parameters['cifs_path']
+ }
+ }
+ if 'cifs_server' in self.parameters:
+ body['target.server'] = self.parameters['cifs_server']
+ if 'locality' in self.parameters:
+ body['target.locality'] = self.parameters['locality']
+ if 'home_directory' in self.parameters:
+ body['target.home_directory'] = self.parameters['home_directory']
+
+ dummy, error = rest_generic.post_async(self.rest_api, api, body)
+ if error is not None:
+ self.module.fail_json(msg='Error while creating cifs unix symlink mapping: %s' % to_native(error),
+ exception=traceback.format_exc())
+
+ def modify_symlink_mapping_rest(self, modify):
+ """
+ Updates a specific UNIX symbolink mapping for a SVM
+ """
+ api = 'protocols/cifs/unix-symlink-mapping/%s/%s' % (self.svm_uuid, self.encode_path(self.parameters['unix_path']))
+ body = {'target': {}}
+ for key, option in [
+ ('share', 'share_name'),
+ ('path', 'cifs_path'),
+ ('server', 'cifs_server'),
+ ('locality', 'locality'),
+ ('home_directory', 'home_directory'),
+ ]:
+ if modify.get(option) is not None:
+ body['target'][key] = modify[option]
+
+ dummy, error = rest_generic.patch_async(self.rest_api, api, uuid_or_name=None, body=body)
+ if error:
+ self.module.fail_json(msg='Error while modifying cifs unix symlink mapping: %s.' % to_native(error),
+ exception=traceback.format_exc())
+
+ def delete_symlink_mapping_rest(self):
+ """
+ Removes a specific UNIX symbolink mapping for a SVM
+ """
+ api = 'protocols/cifs/unix-symlink-mapping/%s/%s' % (self.svm_uuid, self.encode_path(self.parameters['unix_path']))
+ dummy, error = rest_generic.delete_async(self.rest_api, api, uuid=None)
+ if error is not None:
+ self.module.fail_json(msg='Error while deleting cifs unix symlink mapping: %s' % to_native(error))
+
+ def apply(self):
+ # validate leading and trailing forward slashes in unix_path & cifs_path
+ for option in ['unix_path', 'cifs_path']:
+ if self.parameters.get(option) is not None:
+ self.parameters[option] = self.validate_path(self.parameters[option])
+
+ current = self.get_symlink_mapping_rest()
+ cd_action = self.na_helper.get_cd_action(current, self.parameters)
+ modify = self.na_helper.get_modified_attributes(current, self.parameters)
+ if self.na_helper.changed and not self.module.check_mode:
+ if cd_action == 'create':
+ self.create_symlink_mapping_rest()
+ elif cd_action == 'delete':
+ self.delete_symlink_mapping_rest()
+ elif modify:
+ self.modify_symlink_mapping_rest(modify)
+ result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify)
+ self.module.exit_json(**result)
+
+
+def main():
+ symlink_mapping = NetAppOntapCifsUnixSymlink()
+ symlink_mapping.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cli_timeout.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cli_timeout.py
new file mode 100644
index 000000000..02ff00a32
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cli_timeout.py
@@ -0,0 +1,123 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = """
+module: na_ontap_cli_timeout
+short_description: NetApp ONTAP module to set the CLI inactivity timeout value.
+extends_documentation_fragment:
+ - netapp.ontap.netapp.na_ontap
+version_added: '22.9.0'
+author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
+description:
+ - Modify the timeout value for CLI sessions.
+options:
+ state:
+ description:
+ - Modify timeout value, only present is supported.
+ choices: ['present']
+ type: str
+ default: present
+ timeout:
+ description:
+ - Specifies the timeout value, in minutes.
+ - To prevent CLI sessions from timing out, specify a value of 0 (zero).
+ type: int
+ required: true
+
+notes:
+ - Only supported with REST and requires ONTAP 9.6 or later.
+"""
+
+EXAMPLES = """
+ - name: Modify the timeout value for CLI sessions to be 15 minutes
+ netapp.ontap.na_ontap_cli_timeout:
+ state: present
+ timeout: 15
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+ https: true
+ validate_certs: "{{ validate_certs }}"
+
+ - name: Prevent CLI sessions from timing out
+ netapp.ontap.na_ontap_cli_timeout:
+ state: present
+ timeout: 0
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+ https: true
+ validate_certs: "{{ validate_certs }}"
+"""
+
+RETURN = """
+"""
+
+import traceback
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_native
+import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
+from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic
+
+
+class NetAppOntapCliTimeout:
+ def __init__(self):
+ self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
+ self.argument_spec.update(dict(
+ state=dict(required=False, type='str', choices=['present'], default='present'),
+ timeout=dict(required=True, type='int')
+ ))
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ supports_check_mode=True
+ )
+ self.na_helper = NetAppModule(self.module)
+ self.parameters = self.na_helper.check_and_set_parameters(self.module)
+ self.rest_api = netapp_utils.OntapRestAPI(self.module)
+ self.rest_api.fail_if_not_rest_minimum_version('na_ontap_cli_timeout:', 9, 6)
+
+ def get_timeout_value_rest(self):
+ """ Get CLI inactivity timeout value """
+ fields = 'timeout'
+ api = 'private/cli/system/timeout'
+ record, error = rest_generic.get_one_record(self.rest_api, api, query=None, fields=fields)
+ if error:
+ self.module.fail_json(msg="Error fetching CLI sessions timeout value: %s" % to_native(error),
+ exception=traceback.format_exc())
+ if record:
+ return {
+ 'timeout': record.get('timeout')
+ }
+ return None
+
+ def modify_timeout_value_rest(self, modify):
+ """ Modify CLI inactivity timeout value """
+ api = 'private/cli/system/timeout'
+ dummy, error = rest_generic.patch_async(self.rest_api, api, uuid_or_name=None, body=modify)
+ if error:
+ self.module.fail_json(msg='Error modifying CLI sessions timeout value: %s.' % to_native(error),
+ exception=traceback.format_exc())
+
+ def apply(self):
+ current = self.get_timeout_value_rest()
+ modify = self.na_helper.get_modified_attributes(current, self.parameters)
+ if self.na_helper.changed and not self.module.check_mode:
+ self.modify_timeout_value_rest(modify)
+ result = netapp_utils.generate_result(self.na_helper.changed, modify=modify)
+ self.module.exit_json(**result)
+
+
+def main():
+ cli_timeout = NetAppOntapCliTimeout()
+ cli_timeout.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster.py
index fb0f507fc..bac9fd261 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2017-2022, NetApp, Inc
+# (c) 2017-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
@@ -94,7 +94,17 @@ options:
- A system-specific or other term not associated with a geographic region or GMT
- "full list of supported alias can be found here: https://library.netapp.com/ecmdocs/ECMP1155590/html/GUID-D3B8A525-67A2-4BEE-99DB-EF52D6744B5F.html"
- Only supported by REST
-
+ certificate:
+ description:
+ - Certificate used by cluster and node management interfaces for TLS connection requests.
+ - Only supported with REST and requires ONTAP 9.10 or later.
+ type: dict
+ version_added: 22.9.0
+ suboptions:
+ uuid:
+ type: str
+ description:
+ - Certificate UUID.
notes:
- supports REST and ZAPI
'''
@@ -108,6 +118,7 @@ EXAMPLES = """
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
+
- name: Add node to cluster (Join cluster)
netapp.ontap.na_ontap_cluster:
state: present
@@ -115,6 +126,7 @@ EXAMPLES = """
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
+
- name: Add node to cluster (Join cluster)
netapp.ontap.na_ontap_cluster:
state: present
@@ -123,6 +135,7 @@ EXAMPLES = """
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
+
- name: Create a 2 node cluster in one call
netapp.ontap.na_ontap_cluster:
state: present
@@ -131,6 +144,7 @@ EXAMPLES = """
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
+
- name: Remove node from cluster
netapp.ontap.na_ontap_cluster:
state: absent
@@ -138,6 +152,7 @@ EXAMPLES = """
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
+
- name: Remove node from cluster
netapp.ontap.na_ontap_cluster:
state: absent
@@ -145,6 +160,7 @@ EXAMPLES = """
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
+
- name: modify cluster
netapp.ontap.na_ontap_cluster:
state: present
@@ -154,6 +170,19 @@ EXAMPLES = """
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
+
+ - name: updating the cluster-wide web services configuration
+ netapp.ontap.na_ontap_cluster:
+ state: present
+ cluster_contact: testing
+ cluster_location: testing
+ certificate:
+ uuid: 7f2f332c-933e-11ee-ab1c-005056b397ff
+ cluster_name: "{{ netapp_cluster}}"
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+
"""
RETURN = """
@@ -182,6 +211,9 @@ class NetAppONTAPCluster:
cluster_ip_address=dict(required=False, type='str'),
cluster_location=dict(required=False, type='str'),
cluster_contact=dict(required=False, type='str'),
+ certificate=dict(required=False, type='dict', options=dict(
+ uuid=dict(required=False, type='str')
+ )),
force=dict(required=False, type='bool', default=False),
single_node_cluster=dict(required=False, type='bool'),
node_name=dict(required=False, type='str'),
@@ -202,6 +234,10 @@ class NetAppONTAPCluster:
# cached, so that we don't call the REST API more than once
self.node_records = None
+ self.rest_api = OntapRestAPI(self.module)
+ partially_supported_rest_properties = [['certificate', (9, 10, 1)]]
+ self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, None, partially_supported_rest_properties)
+
if self.parameters['state'] == 'absent' and self.parameters.get('node_name') is not None and self.parameters.get('cluster_ip_address') is not None:
msg = 'when state is "absent", parameters are mutually exclusive: cluster_ip_address|node_name'
self.module.fail_json(msg=msg)
@@ -209,14 +245,14 @@ class NetAppONTAPCluster:
if self.parameters.get('node_name') is not None and '-' in self.parameters.get('node_name'):
self.warnings.append('ONTAP ZAPI converts "-" to "_", node_name: %s may be changed or not matched' % self.parameters.get('node_name'))
- self.rest_api = OntapRestAPI(self.module)
- self.use_rest = self.rest_api.is_rest()
if self.use_rest and self.parameters['state'] == 'absent' and not self.rest_api.meets_rest_minimum_version(True, 9, 7, 0):
self.module.warn('switching back to ZAPI as DELETE is not supported on 9.6')
self.use_rest = False
if not self.use_rest:
if self.na_helper.safe_get(self.parameters, ['timezone', 'name']):
self.module.fail_json(msg='Timezone is only supported with REST')
+ if self.na_helper.safe_get(self.parameters, ['certificate', 'uuid']):
+ self.module.fail_json(msg='Certificate is only supported with REST')
if not netapp_utils.has_netapp_lib():
self.module.fail_json(msg="the python NetApp-Lib module is required")
self.server = netapp_utils.setup_na_ontap_zapi(module=self.module)
@@ -235,12 +271,17 @@ class NetAppONTAPCluster:
self.module.fail_json(msg='Error fetching cluster identity info: %s' % to_native(error),
exception=traceback.format_exc())
if record:
- return {
+ cluster_info = {
'cluster_contact': record.get('contact'),
'cluster_location': record.get('location'),
'cluster_name': record.get('name'),
'timezone': self.na_helper.safe_get(record, ['timezone'])
}
+ if self.parameters.get('certificate') is not None:
+ web_service_record = self.get_web_services()
+ cluster_info.update(web_service_record)
+ if cluster_info:
+ return cluster_info
return None
def get_cluster_identity(self, ignore_error=True):
@@ -526,6 +567,27 @@ class NetAppONTAPCluster:
exception=traceback.format_exc())
return uuid, from_node
+ def get_web_services(self):
+ record, error = rest_generic.get_one_record(self.rest_api, 'cluster/web', fields='certificate')
+ if error:
+ self.module.fail_json(msg='Error fetching cluster web service config: %s' % to_native(error),
+ exception=traceback.format_exc())
+ if record:
+ return record
+ return None
+
+ def modify_web_services(self):
+ body = {
+ 'certificate': {
+ 'uuid': self.parameters['certificate']['uuid']
+ }
+ }
+ dummy, error = rest_generic.patch_async(self.rest_api, 'cluster/web', None, body)
+ if error:
+ self.module.fail_json(msg='Error modifying cluster web service config for %s: %s'
+ % (self.parameters['cluster_name'], to_native(error)),
+ exception=traceback.format_exc())
+
def remove_node_rest(self):
"""
Remove a node from an existing cluster
@@ -570,10 +632,12 @@ class NetAppONTAPCluster:
"""
Modifies the cluster identity
"""
+ if 'certificate' in modify:
+ self.modify_web_services()
body = self.create_cluster_body(modify)
dummy, error = rest_generic.patch_async(self.rest_api, 'cluster', None, body)
if error:
- self.module.fail_json(msg='Error modifying cluster idetity details %s: %s'
+ self.module.fail_json(msg='Error modifying cluster identity details %s: %s'
% (self.parameters['cluster_name'], to_native(error)),
exception=traceback.format_exc())
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster_peer.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster_peer.py
index 820001cc4..92c2c419d 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster_peer.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster_peer.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2018-2022, NetApp, Inc
+# (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
@@ -9,6 +9,7 @@ DOCUMENTATION = '''
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
description:
- Create/Delete cluster peer relations on ONTAP
+ - Modify remote intercluster addresses in cluster peer relation on ONTAP
extends_documentation_fragment:
- netapp.ontap.netapp.na_ontap
- netapp.ontap.netapp.na_ontap_peer
@@ -46,12 +47,13 @@ options:
type: str
source_cluster_name:
description:
- - The name of the source cluster name in the peer relation to be deleted.
+ - The name of the source cluster name in the peer relation to be modified or deleted.
+ - Required for deleting peer relation and for modifying source_intercluster_lifs.
type: str
dest_cluster_name:
description:
- - The name of the destination cluster name in the peer relation to be deleted.
- - Required for delete
+ - The name of the destination cluster name in the peer relation to be modified or deleted.
+ - Required for deleting peer relation and for modifying dest_intercluster_lifs.
type: str
dest_hostname:
description:
@@ -86,6 +88,9 @@ options:
version_added: '20.5.0'
short_description: NetApp ONTAP Manage Cluster peering
version_added: 2.7.0
+
+notes:
+ - Modify remote intercluster addresses operation is supported only with REST.
'''
EXAMPLES = """
@@ -129,6 +134,18 @@ EXAMPLES = """
key_filepath: "{{ key_filepath }}"
encryption_protocol_proposed: tls_psk
+ - name: Modify cluster peer - destination intercluster addresses
+ netapp.ontap.na_ontap_cluster_peer:
+ state: present
+ source_intercluster_lifs: 1.2.3.4,1.2.3.5
+ dest_intercluster_lifs: 1.2.3.8
+ dest_cluster_name: test-dest-cluster
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+ peer_options:
+ hostname: "{{ dest_netapp_hostname }}"
+
"""
RETURN = """
@@ -279,7 +296,7 @@ class NetAppONTAPClusterPeer:
# if peer-lifs not present in parameters, use peer_cluster to filter desired cluster peer in current.
if self.parameters.get(peer_lifs) is not None:
peer_addresses_exist = set(self.parameters[peer_lifs]) == set(record['remote']['ip_addresses'])
- else:
+ if self.parameters.get(peer_cluster) is not None:
peer_cluster_exist = self.parameters[peer_cluster] == record['remote']['name']
if peer_addresses_exist or peer_cluster_exist:
cluster_info['cluster_name'] = record['remote']['name']
@@ -379,23 +396,56 @@ class NetAppONTAPClusterPeer:
for record in response['records']:
self.generated_passphrase = record['authentication']['passphrase']
+ def cluster_peer_modify_rest(self, cluster, uuid, modified_peer_addresses):
+ api = 'cluster/peers'
+ body = {'remote.ip_addresses': modified_peer_addresses}
+ server = self.rest_api if cluster == 'source' else self.dst_rest_api
+ dummy, error = rest_generic.patch_async(server, api, uuid, body)
+ if error:
+ self.module.fail_json(msg=error)
+
def apply(self):
"""
Apply action to cluster peer
:return: None
"""
+ modify = {}
source = self.cluster_peer_get('source')
destination = self.cluster_peer_get('destination')
source_action = self.na_helper.get_cd_action(source, self.parameters)
destination_action = self.na_helper.get_cd_action(destination, self.parameters)
self.na_helper.changed = False
+
# create only if expected cluster peer relation is not present on both source and destination clusters
# will error out with appropriate message if peer relationship already exists on either cluster
- if source_action == 'create' or destination_action == 'create':
+ if source_action == 'create' and destination_action == 'create':
if not self.module.check_mode:
self.cluster_peer_create('source')
self.cluster_peer_create('destination')
self.na_helper.changed = True
+ # check and modify IP addresses of the logical interfaces used in peer relation
+ # on either source or destination cluster
+ elif self.use_rest and (source_action is None or destination_action is None):
+ source_changed, destination_changed = False, False
+ if source_action is None:
+ if destination_action == 'create' and self.parameters.get('source_cluster_name') is None:
+ self.module.fail_json(msg='Following option is missing: source_cluster_name')
+ if not self.module.check_mode:
+ if source and (source.get('peer-addresses') != self.parameters.get('dest_intercluster_lifs')):
+ source_changed = True
+ uuid = source['uuid']
+ self.cluster_peer_modify_rest('source', uuid, self.parameters['dest_intercluster_lifs'])
+ modify['dest_intercluster_lifs'] = self.parameters['dest_intercluster_lifs']
+ if destination_action is None:
+ if source_action == 'create' and self.parameters.get('dest_cluster_name') is None:
+ self.module.fail_json(msg='Following option is missing: dest_cluster_name')
+ if not self.module.check_mode:
+ if destination and (destination.get('peer-addresses') != self.parameters.get('source_intercluster_lifs')):
+ destination_changed = True
+ uuid = destination['uuid']
+ self.cluster_peer_modify_rest('destination', uuid, self.parameters['source_intercluster_lifs'])
+ modify['source_intercluster_lifs'] = self.parameters['source_intercluster_lifs']
+ self.na_helper.changed = source_changed | destination_changed
# delete peer relation in cluster where relation is present
else:
if source_action == 'delete':
@@ -409,8 +459,8 @@ class NetAppONTAPClusterPeer:
self.cluster_peer_delete('destination', uuid)
self.na_helper.changed = True
- result = netapp_utils.generate_result(self.na_helper.changed, extra_responses={'source_action': source_action,
- 'destination_action': destination_action})
+ result = netapp_utils.generate_result(self.na_helper.changed, modify=modify, extra_responses={'source_action': source_action,
+ 'destination_action': destination_action})
self.module.exit_json(**result)
@@ -419,8 +469,8 @@ def main():
Execute action
:return: None
"""
- community_obj = NetAppONTAPClusterPeer()
- community_obj.apply()
+ cluster_peer_obj = NetAppONTAPClusterPeer()
+ cluster_peer_obj.apply()
if __name__ == '__main__':
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_dns.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_dns.py
index 67d23cffd..3c46b0084 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_dns.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_dns.py
@@ -259,10 +259,14 @@ class NetAppOntapDns:
if error:
self.module.fail_json(msg="Error getting DNS service: %s" % error)
if record:
+ if params.get('scope') == 'cluster':
+ uuid = record.get('uuid')
+ else:
+ uuid = self.na_helper.safe_get(record, ['svm', 'uuid'])
return {
- 'domains': record['domains'],
- 'nameservers': record['servers'],
- 'uuid': record['svm']['uuid']
+ 'domains': record.get('domains'),
+ 'nameservers': record.get('servers'),
+ 'uuid': uuid
}
if self.parameters.get('vserver') and not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 9, 1):
# There is a chance we are working at the cluster level
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_config.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_config.py
new file mode 100644
index 000000000..30a80e574
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_config.py
@@ -0,0 +1,186 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = """
+module: na_ontap_ems_config
+short_description: NetApp ONTAP module to modify EMS configuration.
+extends_documentation_fragment:
+ - netapp.ontap.netapp.na_ontap
+version_added: '22.8.0'
+author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
+description:
+ - Configure event notification and logging for the cluster.
+options:
+ state:
+ description:
+ - modify EMS configuration, only present is supported.
+ choices: ['present']
+ type: str
+ default: present
+ mail_from:
+ description:
+ - The email address that the event notification system uses as the "From" address for email notifications.
+ type: str
+ required: false
+ mail_server:
+ description:
+ - The name or IP address of the SMTP server that the event notification system uses to send email notification of events.
+ type: str
+ required: false
+ proxy_url:
+ description:
+ - HTTP or HTTPS proxy server URL used by rest-api type EMS notification destinations if your organization uses a proxy.
+ type: str
+ required: false
+ proxy_user:
+ description:
+ - User name for the HTTP or HTTPS proxy server if authentication is required.
+ type: str
+ required: false
+ proxy_password:
+ description:
+ - Password for HTTP or HTTPS proxy.
+ type: str
+ required: false
+ pubsub_enabled:
+ description:
+ - Indicates whether or not events are published to the Publish/Subscribe messaging broker.
+ - Requires ONTAP 9.10 or later.
+ type: bool
+ required: false
+
+notes:
+ - Only supported with REST and requires ONTAP 9.6 or later.
+ - Module is not idempotent when proxy_password is set.
+"""
+
+EXAMPLES = """
+ - name: Modify EMS mail config
+ netapp.ontap.na_ontap_ems_config:
+ state: present
+ mail_from: administrator@mycompany.com
+ mail_server: mail.mycompany.com
+ pubsub_enabled: true
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+ https: true
+ validate_certs: "{{ validate_certs }}"
+
+ - name: Modify EMS proxy config
+ netapp.ontap.na_ontap_ems_config:
+ state: present
+ proxy_url: http://proxy.example.com:8080
+ pubsub_enabled: true
+ proxy_user: admin
+ proxy_password: password
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+ https: true
+ validate_certs: "{{ validate_certs }}"
+"""
+
+RETURN = """
+"""
+
+import traceback
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_native
+import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
+from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic
+
+
+class NetAppOntapEmsConfig:
+ def __init__(self):
+ self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
+ self.argument_spec.update(dict(
+ state=dict(required=False, type='str', choices=['present'], default='present'),
+ mail_from=dict(required=False, type='str'),
+ mail_server=dict(required=False, type='str'),
+ proxy_url=dict(required=False, type='str'),
+ proxy_user=dict(required=False, type='str'),
+ proxy_password=dict(required=False, type='str', no_log=True),
+ pubsub_enabled=dict(required=False, type='bool')
+ ))
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ supports_check_mode=False
+ )
+ self.uuid = None
+ self.na_helper = NetAppModule(self.module)
+ self.parameters = self.na_helper.check_and_set_parameters(self.module)
+ self.rest_api = netapp_utils.OntapRestAPI(self.module)
+ self.rest_api.fail_if_not_rest_minimum_version('na_ontap_ems_config:', 9, 6)
+ self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, None, [['pubsub_enabled', (9, 10, 1)]])
+
+ def get_ems_config_rest(self):
+ """Get EMS config details"""
+ fields = 'mail_from,mail_server,proxy_url,proxy_user'
+ if 'pubsub_enabled' in self.parameters and self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 10, 1):
+ fields += ',pubsub_enabled'
+ record, error = rest_generic.get_one_record(self.rest_api, 'support/ems', None, fields)
+ if error:
+ self.module.fail_json(msg="Error fetching EMS config: %s" % to_native(error), exception=traceback.format_exc())
+ if record:
+ return {
+ 'mail_from': record.get('mail_from'),
+ 'mail_server': record.get('mail_server'),
+ 'proxy_url': record.get('proxy_url'),
+ 'proxy_user': record.get('proxy_user'),
+ 'pubsub_enabled': record.get('pubsub_enabled')
+ }
+ return None
+
+ def modify_ems_config_rest(self, modify):
+ """Modify EMS config"""
+ dummy, error = rest_generic.patch_async(self.rest_api, 'support/ems', None, modify)
+ if error:
+ self.module.fail_json(msg='Error modifying EMS config: %s.' % to_native(error), exception=traceback.format_exc())
+
+ def check_proxy_url(self, current):
+ # GET return the proxy url, if configured, along with port number
+ # based on the existing config, append port numnber to input url to
+ # maintain idempotency while modifying config
+ port = None
+ if current.get('proxy_url') is not None:
+ # strip trailing '/' and extract the port no
+ port = current['proxy_url'].rstrip('/').split(':')[-1]
+ pos = self.parameters['proxy_url'].rstrip('/').rfind(':')
+ if self.parameters['proxy_url'][pos + 1] == '/':
+ # port is not mentioned in input proxy URL
+ # if port is present in current url configured then add to the input url
+ if port is not None and port != '':
+ self.parameters['proxy_url'] = "%s:%s" % (self.parameters['proxy_url'].rstrip('/'), port)
+
+ def apply(self):
+ current = self.get_ems_config_rest()
+ if self.parameters.get('proxy_url') not in [None, '']:
+ self.check_proxy_url(current)
+ modify = self.na_helper.get_modified_attributes(current, self.parameters)
+
+ password_changed = False
+ if self.parameters.get('proxy_password') not in [None, '']:
+ modify['proxy_password'] = self.parameters['proxy_password']
+ self.module.warn('Module is not idempotent when proxy_password is set.')
+ password_changed = True
+ if (self.na_helper.changed or password_changed) and not self.module.check_mode:
+ self.modify_ems_config_rest(modify)
+ result = netapp_utils.generate_result(changed=self.na_helper.changed | password_changed, modify=modify)
+ self.module.exit_json(**result)
+
+
+def main():
+ ems_config = NetAppOntapEmsConfig()
+ ems_config.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_destination.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_destination.py
index 76ddfa31b..599c86c74 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_destination.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_destination.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2022, NetApp, Inc
+# (c) 2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
@@ -18,7 +18,7 @@ extends_documentation_fragment:
version_added: 21.23.0
author: Bartosz Bielawski (@bielawb) <bartek.bielawski@live.com>
description:
- - Configure EMS destination. Currently certificate authentication for REST is not supported.
+ - Configure EMS destination.
options:
state:
description:
@@ -48,6 +48,57 @@ options:
required: true
type: list
elements: str
+ certificate:
+ description:
+ - Name of the certificate
+ required: false
+ type: str
+ version_added: 22.8.0
+ ca:
+ description:
+ - Name of the CA certificate
+ required: false
+ type: str
+ version_added: 22.8.0
+ syslog:
+ description:
+ - The parameter is specified when the EMS destination type is C(syslog).
+ required: false
+ version_added: 22.9.0
+ type: dict
+ suboptions:
+ transport:
+ choices: [udp_unencrypted, tcp_unencrypted, tcp_encrypted]
+ description:
+ - Syslog Transport Protocol.
+ type: str
+ default: 'udp_unencrypted'
+ timestamp_format_override:
+ choices: [no_override, rfc_3164, iso_8601_local_time, iso_8601_utc]
+ description:
+ - Syslog Timestamp Format Override.
+ type: str
+ default: 'no_override'
+ hostname_format_override:
+ choices: [no_override, fqdn, hostname_only]
+ description:
+ - Syslog Hostname Format Override.
+ type: str
+ default: 'no_override'
+ message_format:
+ choices: [legacy_netapp, rfc_5424]
+ description:
+ - Syslog Message Format.
+ type: str
+ default: 'legacy_netapp'
+ port:
+ description:
+ - Syslog Port.
+ type: int
+ default: 514
+notes:
+ - Supports check_mode.
+ - This module only supports REST.
'''
EXAMPLES = """
@@ -62,6 +113,38 @@ EXAMPLES = """
username: "{{username}}"
password: "{{password}}"
+ - name: Configure REST EMS destination with a certificate
+ netapp.ontap.na_ontap_ems_destination:
+ state: present
+ name: rest
+ type: rest_api
+ filters: ['important_events']
+ destination: http://my.rest.api/address
+ certificate: my_cert
+ ca: my_cert_ca
+ hostname: "{{hostname}}"
+ username: "{{username}}"
+ password: "{{password}}"
+
+ - name: Configure REST EMS destination with type syslog
+ netapp.ontap.na_ontap_ems_destination:
+ state: present
+ name: syslog_destination
+ type: syslog
+ filters: ['important_events']
+ destination: http://my.rest.api/address
+ certificate: my_cert
+ ca: my_cert_ca
+ syslog:
+ transport: udp_unencrypted
+ port: 514
+ message_format: legacy_netapp
+ hostname_format_override: no_override
+ timestamp_format_override: no_override
+ hostname: "{{hostname}}"
+ username: "{{username}}"
+ password: "{{password}}"
+
- name: Remove email EMS destination
netapp.ontap.na_ontap_ems_destination:
state: absent
@@ -91,17 +174,31 @@ class NetAppOntapEmsDestination:
state=dict(required=False, type='str', choices=['present', 'absent'], default='present'),
name=dict(required=True, type='str'),
type=dict(required=True, type='str', choices=['email', 'syslog', 'rest_api']),
+ syslog=dict(required=False, type='dict',
+ options=dict(
+ transport=dict(required=False, type='str', choices=['udp_unencrypted', 'tcp_unencrypted', 'tcp_encrypted'],
+ default='udp_unencrypted'),
+ port=dict(required=False, type='int', default=514),
+ message_format=dict(required=False, type='str', choices=['legacy_netapp', 'rfc_5424'], default='legacy_netapp'),
+ timestamp_format_override=dict(required=False, type='str',
+ choices=['no_override', 'rfc_3164', 'iso_8601_local_time', 'iso_8601_utc'], default='no_override'),
+ hostname_format_override=dict(required=False, type='str', choices=['no_override', 'fqdn', 'hostname_only'], default='no_override')
+ )),
destination=dict(required=True, type='str'),
- filters=dict(required=True, type='list', elements='str')
+ filters=dict(required=True, type='list', elements='str'),
+ certificate=dict(required=False, type='str'),
+ ca=dict(required=False, type='str'),
))
self.module = AnsibleModule(
argument_spec=self.argument_spec,
+ required_together=[('certificate', 'ca')],
supports_check_mode=True
)
self.na_helper = NetAppModule()
self.parameters = self.na_helper.set_parameters(self.module.params)
self.rest_api = netapp_utils.OntapRestAPI(self.module)
- self.use_rest = self.rest_api.is_rest()
+ partially_supported_rest_properties = [['certificate', (9, 11, 1)], ['syslog', (9, 12, 1)]]
+ self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, partially_supported_rest_properties=partially_supported_rest_properties)
if not self.use_rest:
self.module.fail_json(msg='na_ontap_ems_destination is only supported with REST API')
@@ -116,8 +213,20 @@ class NetAppOntapEmsDestination:
def get_ems_destination(self, name):
api = 'support/ems/destinations'
- fields = 'name,type,destination,filters.name'
- query = dict(name=name, fields=fields)
+ query = {'name': name,
+ 'fields': 'type,'
+ 'destination,'
+ 'filters.name,'
+ 'certificate.ca,'}
+ if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 11, 1):
+ query['fields'] += 'certificate.name,'
+ if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 12, 1):
+ syslog_option_9_12 = ('syslog.transport,'
+ 'syslog.port,'
+ 'syslog.format.message,'
+ 'syslog.format.timestamp_override,'
+ 'syslog.format.hostname_override,')
+ query['fields'] += syslog_option_9_12
record, error = rest_generic.get_one_record(self.rest_api, api, query)
self.fail_on_error(error, 'fetching EMS destination for %s' % name)
if record:
@@ -125,8 +234,18 @@ class NetAppOntapEmsDestination:
'name': self.na_helper.safe_get(record, ['name']),
'type': self.na_helper.safe_get(record, ['type']),
'destination': self.na_helper.safe_get(record, ['destination']),
- 'filters': None
+ 'filters': None,
+ 'certificate': self.na_helper.safe_get(record, ['certificate', 'name']),
+ 'ca': self.na_helper.safe_get(record, ['certificate', 'ca']),
}
+ if record.get('syslog') is not None:
+ current['syslog'] = {
+ 'port': self.na_helper.safe_get(record, ['syslog', 'port']),
+ 'transport': self.na_helper.safe_get(record, ['syslog', 'transport']),
+ 'timestamp_format_override': self.na_helper.safe_get(record, ['syslog', 'format', 'timestamp_override']),
+ 'hostname_format_override': self.na_helper.safe_get(record, ['syslog', 'format', 'hostname_override']),
+ 'message_format': self.na_helper.safe_get(record, ['syslog', 'format', 'message']),
+ }
# 9.9.0 and earlier versions returns rest-api, convert it to rest_api.
if current['type'] and '-' in current['type']:
current['type'] = current['type'].replace('-', '_')
@@ -135,6 +254,24 @@ class NetAppOntapEmsDestination:
return current
return None
+ def get_certificate_serial(self, cert_name):
+ """Retrieve the serial of a certificate"""
+ api = 'security/certificates'
+ query = {
+ 'scope': "cluster",
+ 'type': "client",
+ 'name': cert_name
+ }
+ fields = 'serial_number'
+ record, error = rest_generic.get_one_record(self.rest_api, api, query, fields)
+ if error:
+ self.module.fail_json(msg='Error retrieving certificates: %s' % error)
+
+ if not record:
+ self.module.fail_json(msg='Error certificate not found: %s.'
+ % (self.parameters['certificate']))
+ return record['serial_number']
+
def create_ems_destination(self):
api = 'support/ems/destinations'
name = self.parameters['name']
@@ -144,6 +281,25 @@ class NetAppOntapEmsDestination:
'destination': self.parameters['destination'],
'filters': self.generate_filters_list(self.parameters['filters'])
}
+
+ if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 11, 1):
+ if self.parameters.get('certificate') and self.parameters.get('ca') is not None:
+ body['certificate'] = {
+ 'serial_number': self.get_certificate_serial(self.parameters['certificate']),
+ 'ca': self.parameters['ca'],
+ }
+ if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 12, 1):
+ if self.parameters.get('syslog') is not None:
+ body['syslog'] = {}
+ for key, option in [
+ ('syslog.port', 'port'),
+ ('syslog.transport', 'transport'),
+ ('syslog.format.message', 'message_format'),
+ ('syslog.format.timestamp_override', 'timestamp_format_override'),
+ ('syslog.format.hostname_override', 'hostname_format_override')
+ ]:
+ if self.parameters['syslog'].get(option) is not None:
+ body[key] = self.parameters['syslog'][option]
dummy, error = rest_generic.post_async(self.rest_api, api, body)
self.fail_on_error(error, 'creating EMS destinations for %s' % name)
@@ -159,9 +315,25 @@ class NetAppOntapEmsDestination:
self.create_ems_destination()
else:
body = {}
+ if any(item in modify for item in ['certificate', 'ca']):
+ body['certificate'] = {}
for option in modify:
if option == 'filters':
body[option] = self.generate_filters_list(modify[option])
+ elif option == 'certificate':
+ body[option]['serial_number'] = self.get_certificate_serial(modify[option])
+ elif option == 'ca':
+ body['certificate']['ca'] = modify[option]
+ elif option == 'syslog':
+ for key, option in [
+ ('syslog.port', 'port'),
+ ('syslog.transport', 'transport'),
+ ('syslog.format.message', 'message_format'),
+ ('syslog.format.timestamp_override', 'timestamp_format_override'),
+ ('syslog.format.hostname_override', 'hostname_format_override')
+ ]:
+ if option in modify['syslog']:
+ body[key] = modify['syslog'][option]
else:
body[option] = modify[option]
if body:
@@ -170,10 +342,9 @@ class NetAppOntapEmsDestination:
self.fail_on_error(error, 'modifying EMS destination for %s' % name)
def apply(self):
- name = None
- modify = None
- current = self.get_ems_destination(self.parameters['name'])
name = self.parameters['name']
+ modify = None
+ current = self.get_ems_destination(name)
cd_action = self.na_helper.get_cd_action(current, self.parameters)
if cd_action is None and self.parameters['state'] == 'present':
modify = self.na_helper.get_modified_attributes(current, self.parameters)
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_filter.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_filter.py
index bdd3a73c3..d6ea223d9 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_filter.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_filter.py
@@ -166,67 +166,90 @@ class NetAppOntapEMSFilters:
self.module.fail_json(msg='Error deleting EMS filter %s: %s' % (self.parameters['name'], to_native(error)),
exception=traceback.format_exc())
- def modify_ems_filter(self):
- # only variable other than name is rules, so if we hit this we know rules has been changed
+ def modify_ems_filter(self, desired_rules):
+ post_api = 'support/ems/filters/%s/rules' % self.parameters['name']
api = 'support/ems/filters'
- body = {'rules': self.na_helper.filter_out_none_entries(self.parameters['rules'])}
- dummy, error = rest_generic.patch_async(self.rest_api, api, self.parameters['name'], body)
- if error:
- self.module.fail_json(msg='Error modifying EMS filter %s: %s' % (self.parameters['name'], to_native(error)),
- exception=traceback.format_exc())
+ if desired_rules['patch_rules'] != []:
+ patch_body = {'rules': desired_rules['patch_rules']}
+ dummy, error = rest_generic.patch_async(self.rest_api, api, self.parameters['name'], patch_body)
+ if error:
+ self.module.fail_json(msg='Error modifying EMS filter %s: %s' % (self.parameters['name'], to_native(error)),
+ exception=traceback.format_exc())
+ if desired_rules['post_rules'] != []:
+ for rule in desired_rules['post_rules']:
+ dummy, error = rest_generic.post_async(self.rest_api, post_api, rule)
+ if error:
+ self.module.fail_json(msg='Error modifying EMS filter %s: %s' % (self.parameters['name'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def desired_ems_rules(self, current_rules):
+ # Modify current filter to remove auto added rule of type exclude, from testing it always appears to be the last element
+ current_rules['rules'] = current_rules['rules'][:-1]
+ if self.parameters.get('rules'):
+ input_rules = self.na_helper.filter_out_none_entries(self.parameters['rules'])
+ for i in range(len(input_rules)):
+ input_rules[i]['message_criteria']['severities'] = input_rules[i]['message_criteria']['severities'].lower()
+ matched_idx = []
+ patch_rules = []
+ post_rules = []
+ for rule_dict in current_rules['rules']:
+ for i in range(len(input_rules)):
+ if input_rules[i]['index'] == rule_dict['index']:
+ matched_idx.append(int(input_rules[i]['index']))
+ patch_rules.append(input_rules[i])
+ break
+ else:
+ rule = {'index': rule_dict['index']}
+ rule['type'] = rule_dict.get('type')
+ if 'message_criteria' in rule_dict:
+ rule['message_criteria'] = {}
+ rule['message_criteria']['severities'] = rule_dict.get('message_criteria').get('severities')
+ rule['message_criteria']['name_pattern'] = rule_dict.get('message_criteria').get('name_pattern')
+ patch_rules.append(rule)
+ for i in range(len(input_rules)):
+ if int(input_rules[i]['index']) not in matched_idx:
+ post_rules.append(input_rules[i])
+ desired_rules = {'patch_rules': patch_rules, 'post_rules': post_rules}
+ return desired_rules
+ return None
- def find_modify(self, current):
- # The normal modify will not work for 2 reasons
- # First ems filter will add a new rule at the end that excludes everything that there isn't a rule for
- # Second Options that are not given are returned as '*' in rest
+ def find_modify(self, current, desired_rules):
if not current:
return False
- # Modify Current to remove auto added rule, from testing it always appears to be the last element
- if current.get('rules'):
- current['rules'].pop()
- # Next check if both have no rules
- if current.get('rules') is None and self.parameters.get('rules') is None:
+ # Next check if either one has no rules
+ if current.get('rules') is None or desired_rules is None:
return False
+ modify = False
+ merge_rules = desired_rules['patch_rules'] + desired_rules['post_rules']
# Next let check if rules is the same size if not we need to modify
- if len(current.get('rules')) != len(self.parameters.get('rules')):
+ if len(current.get('rules')) != len(merge_rules):
return True
- # Next let put the current rules in a dictionary by rule number
- current_rules = self.dic_of_rules(current)
- # Now we need to compare each field to see if there is a match
- modify = False
- for rule in self.parameters['rules']:
- # allow modify if a desired rule index may not exist in current rules.
- # when testing found only index 1, 2 are allowed, if try to set index other than this, let REST throw error.
- if current_rules.get(rule['index']) is None:
- modify = True
- break
- # Check if types are the same
- if rule['type'].lower() != current_rules[rule['index']]['type'].lower():
- modify = True
- break
- if rule.get('message_criteria'):
- if rule['message_criteria'].get('severities') and rule['message_criteria']['severities'].lower() != \
- current_rules[rule['index']]['message_criteria']['severities'].lower():
- modify = True
- break
- if rule['message_criteria'].get('name_pattern') and rule['message_criteria']['name_pattern'] != \
- current_rules[rule['index']]['message_criteria']['name_pattern']:
- modify = True
- break
- return modify
+ for i in range(len(current['rules'])):
+ # compare each field to see if there is a mismatch
+ if current['rules'][i]['index'] != merge_rules[i]['index'] or current['rules'][i]['type'] != merge_rules[i]['type']:
+ return True
+ else:
+ # adding default values for fields under message_criteria
+ if merge_rules[i].get('message_criteria') is None:
+ merge_rules[i]['message_criteria'] = {'severities': '*', 'name_pattern': '*'}
+ elif merge_rules[i]['message_criteria'].get('severities') is None:
+ merge_rules[i]['message_criteria']['severities'] = '*'
+ elif merge_rules[i]['message_criteria'].get('name_pattern') is None:
+ merge_rules[i]['message_criteria']['name_pattern'] = '*'
- def dic_of_rules(self, current):
- rules = {}
- for rule in current['rules']:
- rules[rule['index']] = rule
- return rules
+ if current['rules'][i].get('message_criteria').get('name_pattern') != merge_rules[i].get('message_criteria').get('name_pattern'):
+ return True
+ if current['rules'][i].get('message_criteria').get('severities') != merge_rules[i].get('message_criteria').get('severities'):
+ return True
+ return modify
def apply(self):
current = self.get_ems_filter()
cd_action, modify = None, False
cd_action = self.na_helper.get_cd_action(current, self.parameters)
- if cd_action is None:
- modify = self.find_modify(current)
+ if cd_action is None and self.parameters['state'] == 'present':
+ desired_rules = self.desired_ems_rules(current)
+ modify = self.find_modify(current, desired_rules)
if modify:
self.na_helper.changed = True
if self.na_helper.changed and not self.module.check_mode:
@@ -235,7 +258,7 @@ class NetAppOntapEMSFilters:
if cd_action == 'delete':
self.delete_ems_filter()
if modify:
- self.modify_ems_filter()
+ self.modify_ems_filter(desired_rules)
result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify)
self.module.exit_json(**result)
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_export_policy_rule.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_export_policy_rule.py
index 8b9414074..660ef6825 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_export_policy_rule.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_export_policy_rule.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2018-2022, NetApp, Inc
+# (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
@@ -169,6 +169,7 @@ EXAMPLES = """
state: present
name: default123
rule_index: 100
+ vserver: ci_dev
client_match: 0.0.0.0/0
anonymous_user_id: 65521
ro_rule: ntlm
@@ -732,7 +733,8 @@ class NetAppontapExportRule:
elif modify:
self.modify_export_policy_rule(modify, current['rule_index'])
- self.module.exit_json(changed=self.na_helper.changed)
+ result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify)
+ self.module.exit_json(**result)
def main():
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_file_security_permissions_acl.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_file_security_permissions_acl.py
index 277986466..92514d994 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_file_security_permissions_acl.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_file_security_permissions_acl.py
@@ -209,7 +209,7 @@ EXAMPLES = """
path: "{{ file_mount_path }}"
validate_changes: warn
access: access_allow
- # Note, wihout quotes, use a single backslash in AD user names
+ # Note, without quotes, use a single backslash in AD user names
# with quotes, it needs to be escaped as a double backslash
# user: "ANSIBLE_CIFS\\user1"
# we can't show an example with a single backslash as this is a python file, but it works in YAML.
@@ -466,7 +466,8 @@ class NetAppOntapFileSecurityPermissionsACL:
if modify:
self.modify_file_security_permissions_acl()
self.validate_changes(cd_action, modify)
- self.module.exit_json(changed=self.na_helper.changed)
+ result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify)
+ self.module.exit_json(**result)
def validate_changes(self, cd_action, modify):
if self.parameters['validate_changes'] == 'ignore':
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_igroup_initiator.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_igroup_initiator.py
index 7280eb181..18d25f4dd 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_igroup_initiator.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_igroup_initiator.py
@@ -1,7 +1,7 @@
#!/usr/bin/python
''' This is an Ansible module for ONTAP, to manage initiators in an Igroup
- (c) 2019-2022, NetApp, Inc
+ (c) 2019-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
@@ -192,7 +192,7 @@ class NetAppOntapIgroupInitiator(object):
dummy, error = rest_generic.post_async(self.rest_api, api, body)
else:
query = {'allow_delete_while_mapped': self.parameters['force_remove']}
- dummy, error = rest_generic.delete_async(self.rest_api, api, initiator_name, query)
+ dummy, error = rest_generic.delete_async(self.rest_api, api, initiator_name.lower(), query)
if error:
self.module.fail_json(msg="Error modifying igroup initiator %s: %s" % (initiator_name, error))
@@ -201,7 +201,7 @@ class NetAppOntapIgroupInitiator(object):
for initiator in self.parameters['names']:
present = None
initiator = self.na_helper.sanitize_wwn(initiator)
- if initiator in initiators:
+ if initiator.lower() in initiators:
present = True
cd_action = self.na_helper.get_cd_action(present, self.parameters)
if self.na_helper.changed and not self.module.check_mode:
@@ -209,7 +209,8 @@ class NetAppOntapIgroupInitiator(object):
self.modify_initiator(initiator, 'igroup-add')
elif cd_action == 'delete':
self.modify_initiator(initiator, 'igroup-remove')
- self.module.exit_json(changed=self.na_helper.changed)
+ result = netapp_utils.generate_result(self.na_helper.changed, cd_action)
+ self.module.exit_json(**result)
def main():
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_info.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_info.py
index 6591cc9cd..f9060ffc9 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_info.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_info.py
@@ -213,7 +213,7 @@ options:
- missing_vserver_api_error - most likely the API is available at cluster level but not vserver level.
- rpc_error - some queries are failing because the node cannot reach another node in the cluster.
- key_error - a query is failing because the returned data does not contain an expected key.
- - for key errors, make sure to report this in Slack. It may be a change in a new ONTAP version.
+ - for key errors, make sure to report this in Discord. It may be a change in a new ONTAP version.
- other_error - anything not in the above list.
- always will continue on any error, never will fail on any error, they cannot be used with any other keyword.
type: list
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_kerberos_realm.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_kerberos_realm.py
index 9cb4c346b..27362f220 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_kerberos_realm.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_kerberos_realm.py
@@ -1,7 +1,7 @@
#!/usr/bin/python
'''
(c) 2019, Red Hat, Inc
-(c) 2019-2022, NetApp, Inc
+(c) 2019-2023, NetApp, Inc
GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
@@ -65,7 +65,7 @@ options:
description:
- The clock skew in minutes is the tolerance for accepting tickets with time stamps that do not exactly match the host's system clock.
- The default for this parameter is '5' minutes.
- - This option is not supported with REST.
+ - Supported from ONTAP 9.13.1 in REST.
type: str
comment:
@@ -77,14 +77,14 @@ options:
description:
- IP address of the host where the Kerberos administration daemon is running. This is usually the master KDC.
- If this parameter is omitted, the address specified in kdc_ip is used.
- - This option is not supported with REST.
+ - Supported from ONTAP 9.13.1 in REST.
type: str
admin_server_port:
description:
- The TCP port on the Kerberos administration server where the Kerberos administration service is running.
- The default for this parmater is '749'.
- - This option is not supported with REST.
+ - Supported from ONTAP 9.13.1 in REST.
type: str
pw_server_ip:
@@ -92,14 +92,14 @@ options:
- IP address of the host where the Kerberos password-changing server is running.
- Typically, this is the same as the host indicated in the adminserver-ip.
- If this parameter is omitted, the IP address in kdc-ip is used.
- - This option is not supported with REST.
+ - Supported from ONTAP 9.13.1 in REST.
type: str
pw_server_port:
description:
- The TCP port on the Kerberos password-changing server where the Kerberos password-changing service is running.
- The default for this parameter is '464'.
- - This option is not supported with REST.
+ - Supported from ONTAP 9.13.1 in REST.
type: str
ad_server_ip:
@@ -145,6 +145,21 @@ EXAMPLES = '''
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
+ - name: Create kerberos realm other kdc vendor - REST
+ netapp.ontap.na_ontap_kerberos_realm:
+ state: present
+ realm: 'EXAMPLE.COM'
+ vserver: 'vserver1'
+ kdc_ip: '1.2.3.4'
+ kdc_vendor: 'other'
+ pw_server_ip: '0.0.0.0'
+ pw_server_port: '5'
+ admin_server_ip: '1.2.3.4'
+ admin_server_port: '2'
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+
'''
RETURN = '''
@@ -195,8 +210,9 @@ class NetAppOntapKerberosRealm:
self.parameters = self.na_helper.set_parameters(self.module.params)
# Set up Rest API
self.rest_api = netapp_utils.OntapRestAPI(self.module)
- unsupported_rest_properties = ['admin_server_ip', 'admin_server_port', 'clock_skew', 'pw_server_ip', 'pw_server_port']
- self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, unsupported_rest_properties)
+ partially_supported_rest_properties = [['admin_server_ip', (9, 13, 1)], ['admin_server_port', (9, 13, 1)], ['clock_skew', (9, 13, 1)],
+ ['pw_server_ip', (9, 13, 1)], ['pw_server_port', (9, 13, 1)]]
+ self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, None, partially_supported_rest_properties)
self.svm_uuid = None
if not self.use_rest:
@@ -350,7 +366,7 @@ class NetAppOntapKerberosRealm:
params = {
'name': self.parameters['realm'],
'svm.name': self.parameters['vserver'],
- 'fields': 'kdc,ad_server,svm,comment'
+ 'fields': 'kdc,ad_server,svm,comment,password_server,admin_server,clock_skew'
}
record, error = rest_generic.get_one_record(self.rest_api, api, params)
if error:
@@ -363,7 +379,12 @@ class NetAppOntapKerberosRealm:
'kdc_vendor': self.na_helper.safe_get(record, ['kdc', 'vendor']),
'ad_server_ip': self.na_helper.safe_get(record, ['ad_server', 'address']),
'ad_server_name': self.na_helper.safe_get(record, ['ad_server', 'name']),
- 'comment': self.na_helper.safe_get(record, ['comment'])
+ 'comment': self.na_helper.safe_get(record, ['comment']),
+ 'pw_server_ip': self.na_helper.safe_get(record, ['password_server', 'address']),
+ 'pw_server_port': str(self.na_helper.safe_get(record, ['password_server', 'port'])),
+ 'admin_server_ip': self.na_helper.safe_get(record, ['admin_server', 'address']),
+ 'admin_server_port': str(self.na_helper.safe_get(record, ['admin_server', 'port'])),
+ 'clock_skew': str(self.na_helper.safe_get(record, ['clock_skew']))
}
return None
@@ -379,9 +400,20 @@ class NetAppOntapKerberosRealm:
body['kdc.port'] = self.parameters['kdc_port']
if self.parameters.get('comment'):
body['comment'] = self.parameters['comment']
- if self.parameters['kdc_vendor'] == 'microsoft':
+ if self.parameters.get('ad_server_ip'):
body['ad_server.address'] = self.parameters['ad_server_ip']
+ if self.parameters.get('ad_server_name'):
body['ad_server.name'] = self.parameters['ad_server_name']
+ if self.parameters.get('admin_server_port'):
+ body['admin_server.port'] = self.parameters['admin_server_port']
+ if self.parameters.get('pw_server_port'):
+ body['password_server.port'] = self.parameters['pw_server_port']
+ if self.parameters.get('clock_skew'):
+ body['clock_skew'] = self.parameters['clock_skew']
+ if self.parameters.get('admin_server_ip'):
+ body['admin_server.address'] = self.parameters['admin_server_ip']
+ if self.parameters.get('pw_server_ip'):
+ body['password_server.address'] = self.parameters['pw_server_ip']
dummy, error = rest_generic.post_async(self.rest_api, api, body)
if error:
self.module.fail_json(msg='Error creating Kerberos Realm configuration %s: %s' % (self.parameters['realm'], to_native(error)))
@@ -401,6 +433,16 @@ class NetAppOntapKerberosRealm:
body['ad_server.address'] = modify['ad_server_ip']
if modify.get('ad_server_name'):
body['ad_server.name'] = modify['ad_server_name']
+ if modify.get('admin_server_ip'):
+ body['admin_server.address'] = modify['admin_server_ip']
+ if modify.get('admin_server_port'):
+ body['admin_server.port'] = modify['admin_server_port']
+ if modify.get('pw_server_ip'):
+ body['password_server.address'] = modify['pw_server_ip']
+ if modify.get('pw_server_port'):
+ body['password_server.port'] = modify['pw_server_port']
+ if modify.get('clock_skew'):
+ body['clock_skew'] = modify['clock_skew']
dummy, error = rest_generic.patch_async(self.rest_api, api, self.parameters['realm'], body)
if error:
self.module.fail_json(msg='Error modifying Kerberos Realm %s: %s' % (self.parameters['realm'], to_native(error)))
@@ -416,7 +458,6 @@ class NetAppOntapKerberosRealm:
current = self.get_krbrealm()
cd_action = self.na_helper.get_cd_action(current, self.parameters)
modify = self.na_helper.get_modified_attributes(current, self.parameters)
-
if self.na_helper.changed and not self.module.check_mode:
if cd_action == 'create':
self.create_krbrealm()
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_login_messages.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_login_messages.py
index 099cea8b9..49ad2080a 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_login_messages.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_login_messages.py
@@ -154,12 +154,15 @@ class NetAppOntapLoginMessages:
}
def form_current(self, record):
+ show_cluster_motd = True
+ if record and record.get('show_cluster_message') is not None:
+ show_cluster_motd = record.get('show_cluster_message')
return_result = {
'banner': '',
'motd_message': '',
# we need the SVM UUID to add banner or motd if they are not present
'uuid': record['uuid'] if record else self.get_svm_uuid(self.parameters.get('vserver')),
- 'show_cluster_motd': record.get('show_cluster_message') if record else None
+ 'show_cluster_motd': show_cluster_motd
}
# by default REST adds a trailing \n if no trailing \n set in desired message/banner.
# rstip \n only when desired message/banner does not have trailing \n to preserve idempotency.
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun.py
index c0fb796f7..03ea4f592 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2017-2022, NetApp, Inc
+# (c) 2017-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
@@ -48,6 +48,14 @@ options:
- Not allowed if san_application_template is present.
type: str
+ qtree_name:
+ description:
+ - Specifies the name of the Qtree that contains the new LUN.
+ - Not allowed if san_application_template is present.
+ - Only supported with REST.
+ version_added: 22.8.0
+ type: str
+
size:
description:
- The size of the LUN in C(size_unit).
@@ -336,6 +344,7 @@ class NetAppOntapLUN:
force_remove=dict(required=False, type='bool', default=False),
force_remove_fenced=dict(type='bool'),
flexvol_name=dict(type='str'),
+ qtree_name=dict(type='str'),
vserver=dict(required=True, type='str'),
os_type=dict(required=False, type='str', aliases=['ostype']),
qos_policy_group=dict(required=False, type='str'),
@@ -418,6 +427,8 @@ class NetAppOntapLUN:
if use_application_template:
if self.parameters.get('flexvol_name') is not None:
self.module.fail_json(msg="'flexvol_name' option is not supported when san_application_template is present")
+ if self.parameters.get('qtree_name') is not None:
+ self.module.fail_json(msg="'qtree_name' option is not supported when san_application_template is present")
name = self.na_helper.safe_get(self.parameters, ['san_application_template', 'name'], allow_sparse_dict=False)
rest_app = RestApplication(self.rest_api, self.parameters['vserver'], name)
elif self.parameters.get('flexvol_name') is None:
@@ -914,6 +925,8 @@ class NetAppOntapLUN:
query['name'] = lun_path
else:
query['location.volume.name'] = self.parameters['flexvol_name']
+ if self.parameters.get('qtree_name') is not None:
+ query['location.qtree.name'] = self.parameters['qtree_name']
record, error = rest_generic.get_0_or_more_records(self.rest_api, api, query)
if error:
if lun_path is not None:
@@ -956,6 +969,8 @@ class NetAppOntapLUN:
}
if self.parameters.get('flexvol_name') is not None:
body['location.volume.name'] = self.parameters['flexvol_name']
+ if self.parameters.get('qtree_name') is not None:
+ body['location.qtree.name'] = self.parameters['qtree_name']
if self.parameters.get('os_type') is not None:
body['os_type'] = self.parameters['os_type']
if self.parameters.get('size') is not None:
@@ -978,7 +993,9 @@ class NetAppOntapLUN:
If the name start with a slash we will assume it a path and use it as the name
"""
if not self.parameters['name'].startswith('/') and self.parameters.get('flexvol_name') is not None:
- # if it dosn't start with a slash and we have a flexvol name we will use it to build the path
+ # if it dosn't start with a slash we will use flexvol name and/or qtree name to build the path
+ if self.parameters.get('qtree_name') is not None:
+ return '/vol/%s/%s/%s' % (self.parameters['flexvol_name'], self.parameters['qtree_name'], self.parameters['name'])
return '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name'])
return self.parameters['name']
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map.py
index 5bdbc17c8..67acb4b38 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map.py
@@ -2,7 +2,7 @@
""" this is lun mapping module
- (c) 2018-2022, NetApp, Inc
+ (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
@@ -146,9 +146,7 @@ class NetAppOntapLUNMap:
],
supports_check_mode=True
)
- self.result = dict(
- changed=False,
- )
+ self.lun_info = dict()
self.na_helper = NetAppModule()
self.parameters = self.na_helper.set_parameters(self.module.params)
@@ -337,19 +335,19 @@ class NetAppOntapLUNMap:
if modify:
self.module.fail_json(msg="Modification of lun_map not allowed")
if self.parameters['state'] == 'present' and lun_details:
- self.result.update(lun_details)
- self.result['changed'] = self.na_helper.changed
+ self.lun_info.update(lun_details)
if self.na_helper.changed and not self.module.check_mode:
if cd_action == 'create':
self.create_lun_map()
if cd_action == 'delete':
self.delete_lun_map()
- self.module.exit_json(**self.result)
+ result = netapp_utils.generate_result(self.na_helper.changed, cd_action, extra_responses=self.lun_info)
+ self.module.exit_json(**result)
def main():
- v = NetAppOntapLUNMap()
- v.apply()
+ lun_mapping = NetAppOntapLUNMap()
+ lun_mapping.apply()
if __name__ == '__main__':
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map_reporting_nodes.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map_reporting_nodes.py
index 607c8c430..5763a3d66 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map_reporting_nodes.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map_reporting_nodes.py
@@ -1,7 +1,7 @@
#!/usr/bin/python
"""
- (c) 2018-2022, NetApp, Inc
+ (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
@@ -62,9 +62,9 @@ notes:
EXAMPLES = """
- name: Create Lun Map reporting nodes
netapp.ontap.na_ontap_lun_map_reporting_nodes:
- hostname: 172.21.121.82
- username: admin
- password: netapp1!
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
https: true
validate_certs: false
vserver: vs1
@@ -75,9 +75,9 @@ EXAMPLES = """
- name: Delete Lun Map reporting nodes
netapp.ontap.na_ontap_lun_map_reporting_nodes:
- hostname: 172.21.121.82
- username: admin
- password: netapp1!
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
https: true
validate_certs: false
vserver: vs1
@@ -248,21 +248,27 @@ class NetAppOntapLUNMapReportingNodes:
else:
nodes_to_add = list()
nodes_to_delete = [node for node in self.parameters['nodes'] if node in reporting_nodes]
+ cd_action = None
changed = len(nodes_to_add) > 0 or len(nodes_to_delete) > 0
if changed and not self.module.check_mode:
if nodes_to_add:
+ cd_action = 'add_node'
if self.use_rest:
for node in nodes_to_add:
self.add_lun_map_reporting_nodes_rest(node)
else:
self.add_lun_map_reporting_nodes(nodes_to_add)
if nodes_to_delete:
+ cd_action = 'remove_node'
if self.use_rest:
for node in nodes_to_delete:
self.remove_lun_map_reporting_nodes_rest(node)
else:
self.remove_lun_map_reporting_nodes(nodes_to_delete)
- self.module.exit_json(changed=changed, reporting_nodes=reporting_nodes, nodes_to_add=nodes_to_add, nodes_to_delete=nodes_to_delete)
+ result = netapp_utils.generate_result(changed, cd_action, extra_responses={'reporting_nodes': reporting_nodes,
+ 'nodes_to_add': nodes_to_add,
+ 'nodes_to_delete': nodes_to_delete})
+ self.module.exit_json(**result)
def main():
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_name_mappings.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_name_mappings.py
index 3aa4f2df5..f6afb1546 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_name_mappings.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_name_mappings.py
@@ -273,7 +273,11 @@ class NetAppOntapNameMappings:
self.delete_name_mappings_rest()
elif modify or reindex:
self.modify_name_mappings_rest(modify, reindex)
- self.module.exit_json(changed=self.na_helper.changed)
+ if reindex:
+ modify['new_index'] = self.parameters.get('index')
+ modify['from_index'] = self.parameters['from_index']
+ result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify)
+ self.module.exit_json(**result)
def main():
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_net_ifgrp.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_net_ifgrp.py
index 6ba4083e5..45035ed64 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_net_ifgrp.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_net_ifgrp.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2018-2021, NetApp, Inc
+# (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
@@ -295,7 +295,7 @@ class NetAppOntapIfGrp:
}
return return_value
- def get_if_grp_rest(self, ports, allow_partial_match):
+ def get_if_grp_rest(self, ports, allow_partial_match, force=False):
api = 'network/ethernet/ports'
query = {
'type': 'lag',
@@ -303,7 +303,7 @@ class NetAppOntapIfGrp:
}
fields = 'name,node,uuid,broadcast_domain,lag'
error = None
- if not self.current_records:
+ if not self.current_records or force:
self.current_records, error = rest_generic.get_0_or_more_records(self.rest_api, api, query, fields)
if error:
self.module.fail_json(msg=error)
@@ -342,6 +342,7 @@ class NetAppOntapIfGrp:
current = {
'node': record['node']['name'],
'uuid': record['uuid'],
+ 'name': record['name'],
'ports': current_port_list
}
if record.get('broadcast_domain'):
@@ -496,6 +497,7 @@ class NetAppOntapIfGrp:
def apply(self):
# for a LAG, rename is equivalent to adding/removing ports from an existing LAG.
current, exact_match, modify, rename = None, True, None, None
+ response = None
if not self.use_rest:
current = self.get_if_grp()
elif self.use_rest:
@@ -523,6 +525,9 @@ class NetAppOntapIfGrp:
uuid = current['uuid'] if current and self.use_rest else None
if cd_action == 'create':
self.create_if_grp()
+ # While using REST, fetch the name of the created LAG and return as response in result
+ if self.use_rest:
+ response, exact_match = self.get_if_grp_rest(self.parameters.get('ports'), allow_partial_match=True, force=True)
elif cd_action == 'delete':
self.delete_if_grp(uuid)
elif modify:
@@ -530,7 +535,7 @@ class NetAppOntapIfGrp:
self.modify_ports_rest(modify, uuid)
else:
self.modify_ports(current_ports['ports'])
- result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify)
+ result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify, response=response)
self.module.exit_json(**result)
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_nfs.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_nfs.py
index a1315df1b..9c4c5911f 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_nfs.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_nfs.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2018-2022, NetApp, Inc
+# (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
@@ -555,6 +555,9 @@ class NetAppONTAPNFS:
if error:
self.module.fail_json(msg='Error getting nfs services for SVM %s: %s' % (self.parameters['vserver'], to_native(error)),
exception=traceback.format_exc())
+ if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 11, 0):
+ if record and 'default_user' not in record.get('windows'):
+ record['windows']['default_user'] = None
return self.format_get_nfs_service_rest(record) if record else record
def format_get_nfs_service_rest(self, record):
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_node.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_node.py
index ced6f44be..f88a8827b 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_node.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_node.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2018-2019, NetApp, Inc
+# (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
@@ -83,6 +83,7 @@ import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_ut
from ansible_collections.netapp.ontap.plugins.module_utils.netapp import OntapRestAPI
from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule
import ansible_collections.netapp.ontap.plugins.module_utils.rest_response_helpers as rrh
+import copy
HAS_NETAPP_LIB = netapp_utils.has_netapp_lib()
@@ -218,7 +219,7 @@ class NetAppOntapNode(object):
def apply(self):
from_exists = None
- modify = None
+ modify, modify_dict = None, None
uuid = None
current = self.get_node(self.parameters['name'])
if current is None and 'from_name' in self.parameters:
@@ -235,8 +236,10 @@ class NetAppOntapNode(object):
allowed_options = ['name', 'location']
if not self.use_rest:
allowed_options.append('asset_tag')
- if modify and any(x not in allowed_options for x in modify):
- self.module.fail_json(msg='Too many modified attributes found: %s, allowed: %s' % (modify, allowed_options))
+ if modify:
+ if any(x not in allowed_options for x in modify):
+ self.module.fail_json(msg='Too many modified attributes found: %s, allowed: %s' % (modify, allowed_options))
+ modify_dict = copy.deepcopy(modify)
if current is None and from_exists is None:
msg = 'from_name: %s' % self.parameters.get('from_name') if 'from_name' in self.parameters \
else 'name: %s' % self.parameters['name']
@@ -249,8 +252,8 @@ class NetAppOntapNode(object):
modify.pop('name')
if modify:
self.modify_node(modify, uuid)
-
- self.module.exit_json(changed=self.na_helper.changed)
+ result = netapp_utils.generate_result(self.na_helper.changed, modify=modify_dict)
+ self.module.exit_json(**result)
def main():
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_qos_policy_group.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_qos_policy_group.py
index 8628efd46..b092848ac 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_qos_policy_group.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_qos_policy_group.py
@@ -145,6 +145,22 @@ options:
required: false
choices: ['any', '4k', '8k', '16k', '32k', '64k', '128k']
version_added: 22.6.0
+ expected_iops_allocation:
+ description:
+ - Specifies the size to be used to calculate expected IOPS per TB.
+ - Supported only with REST; requires ONTAP 9.10.1 or later.
+ type: str
+ required: false
+ choices: ['used_space', 'allocated_space']
+ version_added: 22.8.0
+ peak_iops_allocation:
+ description:
+ - Specifies the size to be used to calculate peak IOPS per TB.
+ - Supported only with REST; requires ONTAP 9.10.1 or later.
+ type: str
+ required: false
+ choices: ['used_space', 'allocated_space']
+ version_added: 22.8.0
'''
EXAMPLES = """
@@ -224,6 +240,18 @@ EXAMPLES = """
expected_iops: 200
peak_iops: 500
+ - name: modify adaptive qos policy group in REST.
+ netapp.ontap.na_ontap_qos_policy_group:
+ state: present
+ name: adaptive_policy
+ vserver: policy_vserver
+ hostname: 10.193.78.30
+ username: admin
+ password: netapp1!
+ use_rest: always
+ adaptive_qos_options:
+ expected_iops_allocation: used_space
+ peak_iops_allocation: allocated_space
"""
RETURN = """
@@ -268,7 +296,9 @@ class NetAppOntapQosPolicyGroup:
absolute_min_iops=dict(required=True, type='int'),
expected_iops=dict(required=True, type='int'),
peak_iops=dict(required=True, type='int'),
- block_size=dict(required=False, type='str', choices=['any', '4k', '8k', '16k', '32k', '64k', '128k'])
+ block_size=dict(required=False, type='str', choices=['any', '4k', '8k', '16k', '32k', '64k', '128k']),
+ expected_iops_allocation=dict(required=False, type='str', choices=['used_space', 'allocated_space']),
+ peak_iops_allocation=dict(required=False, type='str', choices=['used_space', 'allocated_space'])
))
))
@@ -297,9 +327,11 @@ class NetAppOntapQosPolicyGroup:
if not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 8) and \
self.na_helper.safe_get(self.parameters, ['fixed_qos_options', 'min_throughput_mbps']):
self.module.fail_json(msg="Minimum version of ONTAP for 'fixed_qos_options.min_throughput_mbps' is (9, 8, 0)")
+
+ ontap_9_10_adaptive_options = ['block_size', 'expected_iops_allocation', 'peak_iops_allocation']
if not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 10, 1) and \
- self.na_helper.safe_get(self.parameters, ['adaptive_qos_options', 'block_size']):
- self.module.fail_json(msg="Minimum version of ONTAP for 'adaptive_qos_options.block_size' is (9, 10, 1)")
+ any(self.na_helper.safe_get(self.parameters, ['adaptive_qos_options', option]) for option in ontap_9_10_adaptive_options):
+ self.module.fail_json(msg='Error: %s' % self.rest_api.options_require_ontap_version(ontap_9_10_adaptive_options, version='9.10.1'))
self.uuid = None
if not self.use_rest:
@@ -383,7 +415,8 @@ class NetAppOntapQosPolicyGroup:
if 'adaptive' in record:
current['adaptive_qos_options'] = {}
- for adaptive_qos_option in ['absolute_min_iops', 'expected_iops', 'peak_iops', 'block_size']:
+ for adaptive_qos_option in ['absolute_min_iops', 'expected_iops', 'peak_iops', 'block_size',
+ 'expected_iops_allocation', 'peak_iops_allocation']:
current['adaptive_qos_options'][adaptive_qos_option] = record['adaptive'].get(adaptive_qos_option)
return current
@@ -479,6 +512,11 @@ class NetAppOntapQosPolicyGroup:
if 'fixed_qos_options' in modify:
body['fixed'] = modify['fixed_qos_options']
else:
+ if 'block_size' not in self.na_helper.safe_get(modify, ['adaptive_qos_options']) and \
+ self.na_helper.safe_get(self.parameters, ['adaptive_qos_options', 'block_size']) is None:
+ # if block_size is not to be modified then remove it from the params
+ # to avoid error with block_size option during modification of other adaptive qos options
+ del self.parameters['adaptive_qos_options']['block_size']
body['adaptive'] = self.parameters['adaptive_qos_options']
dummy, error = rest_generic.patch_async(self.rest_api, api, self.uuid, body)
if error:
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_rest_info.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_rest_info.py
index b1b5b6dae..029cf40d0 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_rest_info.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_rest_info.py
@@ -293,7 +293,7 @@ options:
version_added: '21.9.0'
owning_resource:
description:
- - Some resources cannot be accessed directly. You need to select them based on the owner or parent. For instance, volume for a snaphot.
+ - Some resources cannot be accessed directly. You need to select them based on the owner or parent. For instance, volume for a snapshot.
- The following subsets require an owning resource, and the following suboptions when uuid is not present.
- <storage/volumes/snapshots> B(volume_name) is the volume name, B(svm_name) is the owning vserver name for the volume.
- <protocols/nfs/export-policies/rules> B(policy_name) is the name of the policy, B(svm_name) is the owning vserver name for the policy,
@@ -310,6 +310,11 @@ options:
type: list
elements: str
version_added: '21.23.0'
+ hal_linking:
+ description:
+ - if false, HAL-encoded links are disabled in the REST calls.
+ default: true
+ type: bool
'''
EXAMPLES = '''
@@ -488,6 +493,7 @@ class NetAppONTAPGatherInfo(object):
use_python_keys=dict(type='bool', default=False),
owning_resource=dict(type='dict', required=False),
ignore_api_errors=dict(type='list', elements='str', required=False),
+ hal_linking=dict(required=False, type='bool', default=True),
))
self.module = AnsibleModule(
@@ -532,7 +538,9 @@ class NetAppONTAPGatherInfo(object):
for each in self.parameters['parameters']:
data[each] = self.parameters['parameters'][each]
- gathered_ontap_info, error = self.rest_api.get(api, data)
+ accept_header = 'application/hal+json' if self.parameters.get('hal_linking') else 'application/json'
+ headers = self.rest_api.build_headers(accept=accept_header)
+ gathered_ontap_info, error = self.rest_api.get(api, data, headers=headers)
if not error:
return gathered_ontap_info
@@ -1091,6 +1099,8 @@ class NetAppONTAPGatherInfo(object):
def add_uuid_subsets(self, get_ontap_subset_info):
params = self.parameters.get('owning_resource')
+ owning_resource_supported_subsets = ['storage/volumes/snapshots', 'protocols/nfs/export-policies/rules',
+ 'protocols/vscan/on-access-policies', 'protocols/vscan/on-demand-policies', 'protocols/vscan/scanner-pools']
if 'gather_subset' in self.parameters:
if 'storage/volumes/snapshots' in self.parameters['gather_subset']:
self.check_error_values('storage/volumes/snapshots', params, ['volume_name', 'svm_name'])
@@ -1112,6 +1122,9 @@ class NetAppONTAPGatherInfo(object):
self.add_vserver_owning_resource('protocols/vscan/on-demand-policies', params, 'protocols/vscan/%s/on-demand-policies', get_ontap_subset_info)
if 'protocols/vscan/scanner-pools' in self.parameters['gather_subset']:
self.add_vserver_owning_resource('protocols/vscan/scanner-pools', params, 'protocols/vscan/%s/scanner-pools', get_ontap_subset_info)
+ owning_resource_warning = any(subset not in owning_resource_supported_subsets for subset in self.parameters['gather_subset'])
+ if owning_resource_warning and params is not None:
+ self.module.warn("Kindly refer to Ansible documentation to check the subsets that support option 'owning_resource'.")
return get_ontap_subset_info
def add_vserver_owning_resource(self, subset, params, api, get_ontap_subset_info):
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_restit.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_restit.py
index 7bfd63b71..fd56307f3 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_restit.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_restit.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
'''
-# (c) 2020, NetApp, Inc
+# (c) 2020-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
@@ -372,13 +372,15 @@ class NetAppONTAPRestAPI(object):
def apply(self):
''' calls the api and returns json output '''
+ changed_status = False if self.method.upper() == 'GET' else True
+
if self.module.check_mode:
status_code, response = None, {'check_mode': 'would run %s %s' % (self.method, self.api)}
elif self.wait_for_completion:
status_code, response = self.run_api_async()
else:
status_code, response = self.run_api()
- self.module.exit_json(changed=True, status_code=status_code, response=response)
+ self.module.exit_json(changed=changed_status, status_code=status_code, response=response)
def main():
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_s3_services.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_s3_services.py
index ff5feb722..e8d8ed994 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_s3_services.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_s3_services.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2018-2022, NetApp, Inc
+# (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
@@ -86,6 +86,19 @@ EXAMPLES = """
RETURN = """
+s3_service_info:
+ description: Returns S3 service response.
+ returned: on creation or modification of S3 service
+ type: dict
+ sample: '{
+ "s3_service_info": {
+ "name": "Service1",
+ "enabled": false,
+ "certificate_name": "testSVM_177966509ABA4EC6",
+ "users": [{"name": "root"}, {"name": "user1", "access_key": "IWE711019OW02ZB3WH6Q"}],
+ "svm": {"name": "testSVM", "uuid": "39c2a5a0-35e2-11ee-b8da-005056b37403"}}
+ }
+ }'
"""
import traceback
@@ -116,21 +129,19 @@ class NetAppOntapS3Services:
self.svm_uuid = None
self.na_helper = NetAppModule(self.module)
self.parameters = self.na_helper.check_and_set_parameters(self.module)
-
self.rest_api = OntapRestAPI(self.module)
- partially_supported_rest_properties = [] # TODO: Remove if there nothing here
- self.use_rest = self.rest_api.is_rest(partially_supported_rest_properties=partially_supported_rest_properties,
- parameters=self.parameters)
-
+ self.use_rest = self.rest_api.is_rest()
self.rest_api.fail_if_not_rest_minimum_version('na_ontap_s3_services', 9, 8)
- def get_s3_service(self):
+ def get_s3_service(self, extra_field=False):
api = 'protocols/s3/services'
fields = ','.join(('name',
'enabled',
'svm.uuid',
'comment',
'certificate.name'))
+ if extra_field:
+ fields += ',users'
params = {
'name': self.parameters['name'],
@@ -192,25 +203,48 @@ class NetAppOntapS3Services:
self.svm_uuid = record['svm']['uuid']
return record
+ def parse_response(self, response):
+ if response is not None:
+ users_info = []
+ options = ['name', 'access_key', 'secret_key']
+ for user_info in response.get('users'):
+ info = {}
+ for option in options:
+ if user_info.get(option) is not None:
+ info[option] = user_info.get(option)
+ users_info.append(info)
+ return {
+ 'name': response.get('name'),
+ 'enabled': response.get('enabled'),
+ 'certificate_name': response.get('certificate_name'),
+ 'users': users_info,
+ 'svm': {'name': self.na_helper.safe_get(response, ['svm', 'name']),
+ 'uuid': self.na_helper.safe_get(response, ['svm', 'uuid'])}
+ }
+ return None
+
def apply(self):
current = self.get_s3_service()
- cd_action, modify = None, None
+ cd_action, modify, response = None, None, None
cd_action = self.na_helper.get_cd_action(current, self.parameters)
if cd_action is None:
modify = self.na_helper.get_modified_attributes(current, self.parameters)
if self.na_helper.changed and not self.module.check_mode:
if cd_action == 'create':
self.create_s3_service()
+ response = self.get_s3_service(True)
if cd_action == 'delete':
self.delete_s3_service()
if modify:
self.modify_s3_service(modify)
- result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify)
+ response = self.get_s3_service(True)
+ message = self.parse_response(response)
+ result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify, extra_responses={'s3_service_info': message})
self.module.exit_json(**result)
def main():
- '''Apply volume operations from playbook'''
+ '''Apply S3 service operations from playbook'''
obj = NetAppOntapS3Services()
obj.apply()
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_security_certificates.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_security_certificates.py
index c7131fe5e..e6fc74e92 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_security_certificates.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_security_certificates.py
@@ -378,8 +378,11 @@ class NetAppOntapSecurityCertificates:
for key in required_keys + optional_keys:
if self.parameters.get(key) is not None:
body[key] = self.parameters[key]
+ params = {
+ "return_records": "true"
+ }
api = "security/certificates"
- message, error = self.rest_api.post(api, body)
+ message, error = self.rest_api.post(api, body, params)
if error:
if self.parameters.get('svm') is None and error.get('target') == 'uuid':
error['target'] = 'cluster'
@@ -399,7 +402,10 @@ class NetAppOntapSecurityCertificates:
for key in optional_keys:
if self.parameters.get(key) is not None:
body[key] = self.parameters[key]
- message, error = self.rest_api.post(api, body)
+ params = {
+ "return_records": "true"
+ }
+ message, error = self.rest_api.post(api, body, params)
if error:
self.module.fail_json(msg="Error signing certificate: %s" % error)
return message
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_service_policy.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_service_policy.py
index f2969f720..395bbb695 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_service_policy.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_service_policy.py
@@ -64,13 +64,14 @@ options:
choices: ['cluster', 'svm']
known_services:
description:
- - List of known services in 9.11.1
+ - List of known services in 9.12.1
- An error is raised if any service in C(services) is not in this list or C(new_services).
- Modify this list to restrict the services you want to support if needed.
default: [cluster_core, intercluster_core, management_core, management_autosupport, management_bgp, management_ems, management_https, management_http,
management_ssh, management_portmap, data_core, data_nfs, data_cifs, data_flexcache, data_iscsi, data_s3_server, data_dns_server,
data_fpolicy_client, management_ntp_client, management_dns_client, management_ad_client, management_ldap_client, management_nis_client,
- management_snmp_server, management_rsh_server, management_telnet_server, management_ntp_server, data_nvme_tcp, backup_ndmp_control]
+ management_snmp_server, management_rsh_server, management_telnet_server, management_ntp_server, data_nvme_tcp, backup_ndmp_control,
+ management_log_forwarding]
type: list
elements: str
version_added: 22.0.0
@@ -184,7 +185,7 @@ class NetAppOntapServicePolicy:
'data_flexcache', 'data_iscsi', 'data_s3_server', 'data_dns_server', 'data_fpolicy_client', 'management_ntp_client',
'management_dns_client', 'management_ad_client', 'management_ldap_client', 'management_nis_client',
'management_snmp_server', 'management_rsh_server', 'management_telnet_server', 'management_ntp_server',
- 'data_nvme_tcp', 'backup_ndmp_control']),
+ 'data_nvme_tcp', 'backup_ndmp_control', 'management_log_forwarding']),
additional_services=dict(type='list', elements='str')
))
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapmirror.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapmirror.py
index 26254e03b..e53358041 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapmirror.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapmirror.py
@@ -595,6 +595,7 @@ class NetAppONTAPSnapmirror(object):
self.na_helper = NetAppModule()
self.parameters = self.na_helper.set_parameters(self.module.params)
+ self.policy_type = None
self.new_style = False
# when deleting, ignore previous errors, but report them if delete fails
self.previous_errors = []
@@ -1051,7 +1052,8 @@ class NetAppONTAPSnapmirror(object):
resync SnapMirror based on relationship state
"""
if self.use_rest:
- self.snapmirror_mod_init_resync_break_quiesce_resume_rest(state="snapmirrored")
+ state = 'in_sync' if self.policy_type == 'sync' else 'snapmirrored'
+ self.snapmirror_mod_init_resync_break_quiesce_resume_rest(state=state)
else:
options = {'destination-location': self.parameters['destination_path']}
snapmirror_resync = netapp_utils.zapi.NaElement.create_node_with_children('snapmirror-resync', **options)
@@ -1067,7 +1069,8 @@ class NetAppONTAPSnapmirror(object):
resume SnapMirror based on relationship state
"""
if self.use_rest:
- return self.snapmirror_mod_init_resync_break_quiesce_resume_rest(state="snapmirrored")
+ state = 'in_sync' if self.policy_type == 'sync' else 'snapmirrored'
+ return self.snapmirror_mod_init_resync_break_quiesce_resume_rest(state=state)
options = {'destination-location': self.parameters['destination_path']}
snapmirror_resume = netapp_utils.zapi.NaElement.create_node_with_children('snapmirror-resume', **options)
@@ -1482,7 +1485,7 @@ class NetAppONTAPSnapmirror(object):
destination = self.parameters['destination_path']
api = 'snapmirror/relationships'
- fields = 'uuid,state,transfer.state,transfer.uuid,policy.name,unhealthy_reason.message,healthy,source'
+ fields = 'uuid,state,transfer.state,transfer.uuid,policy.name,policy.type,unhealthy_reason.message,healthy,source'
if 'schedule' in self.parameters:
fields += ',transfer_schedule'
options = {'destination.path': destination, 'fields': fields}
@@ -1499,9 +1502,10 @@ class NetAppONTAPSnapmirror(object):
snap_info['status'] = self.na_helper.safe_get(record, ['transfer', 'state'])
self.parameters['current_transfer_status'] = self.na_helper.safe_get(record, ['transfer', 'state'])
snap_info['policy'] = self.na_helper.safe_get(record, ['policy', 'name'])
+ self.policy_type = self.na_helper.safe_get(record, ['policy', 'type'])
# REST API supports only Extended Data Protection (XDP) SnapMirror relationship
snap_info['relationship_type'] = 'extended_data_protection'
- # initilized to avoid name keyerror
+ # initialized to avoid name keyerror
snap_info['current_transfer_type'] = ""
snap_info['max_transfer_rate'] = ""
if 'unhealthy_reason' in record:
@@ -1741,8 +1745,8 @@ class NetAppONTAPSnapmirror(object):
def main():
"""Execute action"""
- community_obj = NetAppONTAPSnapmirror()
- community_obj.apply()
+ snapmirror_obj = NetAppONTAPSnapmirror()
+ snapmirror_obj.apply()
if __name__ == '__main__':
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapshot_policy.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapshot_policy.py
index 1d271657a..ee1a63be5 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapshot_policy.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapshot_policy.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2018-2022, NetApp, Inc
+# (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
@@ -611,6 +611,7 @@ class NetAppOntapSnapshotPolicy(object):
api = 'storage/snapshot-policies/%s/schedules' % current['uuid']
schedule_info = self.get_snapshot_schedule_rest(current)
delete_schedules, modify_schedules, add_schedules = [], [], []
+ retain_schedules_count = 0
if 'snapmirror_label' in self.parameters:
snapmirror_labels = self.parameters['snapmirror_label']
@@ -629,6 +630,8 @@ class NetAppOntapSnapshotPolicy(object):
schedule_name = self.safe_strip(schedule_name)
if schedule_name not in [item.strip() for item in self.parameters['schedule']]:
delete_schedules.append(schedule_uuid)
+ else:
+ retain_schedules_count += 1
# Identify schedules to be modified or added
for schedule_name, count, snapmirror_label, prefix in zip(self.parameters['schedule'], self.parameters['count'], snapmirror_labels, prefixes):
@@ -668,9 +671,11 @@ class NetAppOntapSnapshotPolicy(object):
body['prefix'] = prefix
add_schedules.append(body)
- # Delete N-1 schedules no longer required. Must leave 1 schedule in policy
+ # Delete N schedules no longer required if there is at least 1 schedule is to be retained
+ # Otherwise, delete N-1 schedules no longer required as policy must have at least 1 schedule
# at any one time. Delete last one afterwards.
- while len(delete_schedules) > 1:
+ count = 0 if retain_schedules_count > 0 else 1
+ while len(delete_schedules) > count:
schedule_uuid = delete_schedules.pop()
record, error = rest_generic.delete_async(self.rest_api, api, schedule_uuid)
if error is not None:
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp.py
index c1f278e0d..acde02da2 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp.py
@@ -3,7 +3,7 @@
create SNMP module to add/delete/modify SNMP user
"""
-# (c) 2018-2021, NetApp, Inc
+# (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
@@ -12,7 +12,7 @@ __metaclass__ = type
DOCUMENTATION = '''
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
description:
- - "Create/Delete SNMP community"
+ - Create/Delete SNMP user.
extends_documentation_fragment:
- netapp.ontap.netapp.na_ontap
module: na_ontap_snmp
@@ -20,21 +20,60 @@ options:
access_control:
choices: ['ro']
description:
- - "Access control for the community. The only supported value is 'ro' (read-only). Ignored with REST"
+ - Access control for the community. The only supported value is 'ro' (read-only).
+ - Ignored with REST.
default: 'ro'
type: str
- community_name:
+ snmp_username:
description:
- - "The name of the SNMP community to manage."
+ - The name of the SNMP user to manage.
required: true
type: str
+ version_added: 22.8.0
state:
choices: ['present', 'absent']
description:
- - "Whether the specified SNMP community should exist or not."
+ - Whether the specified SNMP user should exist or not.
default: 'present'
type: str
-short_description: NetApp ONTAP SNMP community
+ authentication_method:
+ choices: ['community', 'usm', 'both']
+ description:
+ - Authentication method for SNMP user.
+ - Only supported with REST. The default value is community.
+ type: str
+ version_added: 22.8.0
+ snmpv3:
+ description:
+ - Specify only when C(authentication_method) is either C(usm) or C(both).
+ - This option defines the SNMPv3 credentials for an SNMPv3 user or also called usm user.
+ - Only supported with REST.
+ type: dict
+ version_added: 22.8.0
+ suboptions:
+ authentication_password:
+ description:
+ - Authentication protocol password.
+ type: str
+ required: true
+ authentication_protocol:
+ choices: ['none', 'md5', 'sha', 'sha2_256']
+ description:
+ - Authentication protocol for SNMPv3.
+ default: 'none'
+ type: str
+ privacy_password:
+ description:
+ - Privacy protocol password.
+ type: str
+ required: true
+ privacy_protocol:
+ choices: ['none', 'des', 'aes128']
+ description:
+ - Privacy protocol for SNMPv3.
+ default: 'none'
+ type: str
+short_description: NetApp ONTAP SNMP user
version_added: 2.6.0
'''
@@ -42,34 +81,60 @@ EXAMPLES = """
- name: Create SNMP community (ZAPI only)
netapp.ontap.na_ontap_snmp:
state: present
- community_name: communityName
+ snmp_username: communityName
access_control: 'ro'
use_rest: never
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
+
- name: Create SNMP community (snmpv1 or snmpv2) (REST only)
netapp.ontap.na_ontap_snmp:
state: present
- community_name: communityName
+ snmp_username: communityName
use_rest: always
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
+ - name: Create SNMP user (snmpv3) (REST only)
+ netapp.ontap.na_ontap_snmp:
+ state: present
+ snmp_username: username
+ use_rest: always
+ authentication_method: usm
+ snmpv3:
+ authentication_protocol: sha
+ authentication_password: humTdumt*@t0nAwa21
+ privacy_protocol: aes128
+ privacy_password: p@**GOandCLCt*300
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+
- name: Delete SNMP community (ZAPI only)
netapp.ontap.na_ontap_snmp:
state: absent
- community_name: communityName
+ snmp_username: communityName
access_control: 'ro'
use_rest: never
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
+
- name: Delete SNMP community (snmpv1 or snmpv2) (REST only)
netapp.ontap.na_ontap_snmp:
state: absent
- community_name: communityName
+ snmp_username: communityName
+ use_rest: always
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+
+ - name: Delete SNMP user (snmpv3) (REST only)
+ netapp.ontap.na_ontap_snmp:
+ state: absent
+ snmp_username: username
use_rest: always
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
@@ -96,8 +161,17 @@ class NetAppONTAPSnmp(object):
self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
self.argument_spec.update(dict(
state=dict(required=False, type='str', choices=['present', 'absent'], default='present'),
- community_name=dict(required=True, type='str'),
+ snmp_username=dict(required=True, type='str'),
access_control=dict(required=False, type='str', choices=['ro'], default='ro'),
+ authentication_method=dict(required=False, type='str', choices=['community', 'usm', 'both']),
+ snmpv3=dict(required=False, type='dict',
+ options=dict(
+ authentication_password=dict(required=True, type='str', no_log=True),
+ privacy_protocol=dict(required=False, type='str', choices=['none', 'des', 'aes128'], default='none'),
+ authentication_protocol=dict(required=False, type='str', choices=['none', 'md5', 'sha', 'sha2_256'], default='none'),
+ privacy_password=dict(required=True, type='str', no_log=True),
+ )
+ )
))
self.module = AnsibleModule(
@@ -113,18 +187,29 @@ class NetAppONTAPSnmp(object):
self.rest_api = OntapRestAPI(self.module)
self.use_rest = self.rest_api.is_rest()
+ self.unsupported_zapi_properties = ['authentication_method', 'snmpv3', 'authentication_protocol', 'authentication_password', 'privacy_protocol',
+ 'privacy_password']
+
+ if self.use_rest:
+ if self.parameters.get('authentication_method') == 'community' and 'snmpv3' in self.parameters:
+ self.module.fail_json("SNMPv3 user can be created when 'authentication_method' is either 'usm' or 'both'")
+
if not self.use_rest:
if HAS_NETAPP_LIB is False:
self.module.fail_json(msg="the python NetApp-Lib module is required")
- else:
- self.server = netapp_utils.setup_na_ontap_zapi(module=self.module)
+
+ for unsupported_zapi_property in self.unsupported_zapi_properties:
+ if self.parameters.get(unsupported_zapi_property) is not None:
+ msg = "Error: %s option is not supported with ZAPI. It can only be used with REST." % unsupported_zapi_property
+ self.module.fail_json(msg=msg)
+ self.server = netapp_utils.setup_na_ontap_zapi(module=self.module)
def invoke_snmp_community(self, zapi):
"""
Invoke zapi - add/delete take the same NaElement structure
"""
snmp_community = netapp_utils.zapi.NaElement.create_node_with_children(
- zapi, **{'community': self.parameters['community_name'],
+ zapi, **{'community': self.parameters['snmp_username'],
'access-control': self.parameters['access_control']})
try:
self.server.invoke_successfully(snmp_community, enable_tunneling=True)
@@ -135,7 +220,7 @@ class NetAppONTAPSnmp(object):
action = 'deleting'
else:
action = 'unexpected'
- self.module.fail_json(msg='Error %s community %s: %s' % (action, self.parameters['community_name'], to_native(error)),
+ self.module.fail_json(msg='Error %s community %s: %s' % (action, self.parameters['snmp_username'], to_native(error)),
exception=traceback.format_exc())
def get_snmp(self):
@@ -151,19 +236,19 @@ class NetAppONTAPSnmp(object):
self.module.fail_json(msg=to_native(error), exception=traceback.format_exc())
if result.get_child_by_name('communities') is not None:
for snmp_entry in result.get_child_by_name('communities').get_children():
- community_name = snmp_entry.get_child_content('community')
- if community_name == self.parameters['community_name']:
+ snmp_username = snmp_entry.get_child_content('community')
+ if snmp_username == self.parameters['snmp_username']:
return {
- 'community_name': snmp_entry.get_child_content('community'),
+ 'snmp_username': snmp_entry.get_child_content('community'),
'access_control': snmp_entry.get_child_content('access-control'),
}
return None
def get_snmp_rest(self):
# There can be SNMPv1, SNMPv2 (called community) or
- # SNMPv3 local or SNMPv3 remote (called users)
+ # SNMPv3 (called usm users)
api = 'support/snmp/users'
- params = {'name': self.parameters['community_name'],
+ params = {'name': self.parameters['snmp_username'],
'fields': 'name,engine_id'}
message, error = self.rest_api.get(api, params)
record, error = rrh.check_for_0_or_1_records(api, message, error)
@@ -171,56 +256,56 @@ class NetAppONTAPSnmp(object):
self.module.fail_json(msg=error)
if record:
# access control does not exist in rest
- return dict(community_name=record['name'], engine_id=record['engine_id'], access_control='ro')
+ return dict(snmp_username=record['name'], engine_id=record['engine_id'], access_control='ro')
return None
- def add_snmp_community(self):
+ def add_snmp_user(self):
"""
- Adds a SNMP community
+ Add a SNMP user
"""
if self.use_rest:
- self.add_snmp_community_rest()
+ self.add_snmp_rest()
else:
self.invoke_snmp_community('snmp-community-add')
- def add_snmp_community_rest(self):
+ def add_snmp_rest(self):
api = 'support/snmp/users'
- params = {'name': self.parameters['community_name'],
- 'authentication_method': 'community'}
- message, error = self.rest_api.post(api, params)
+ self.parameters['authentication_method'] = self.parameters.get('authentication_method', 'community')
+ body = {
+ 'name': self.parameters['snmp_username'],
+ 'authentication_method': self.parameters['authentication_method']
+ }
+ if self.parameters.get('authentication_method') == 'usm' or self.parameters.get('authentication_method') == 'both':
+ if self.parameters.get('snmpv3'):
+ body['snmpv3'] = self.parameters['snmpv3']
+ message, error = self.rest_api.post(api, body)
if error:
self.module.fail_json(msg=error)
- def delete_snmp_community(self, current=None):
+ def delete_snmp_user(self, current=None):
"""
- Delete a SNMP community
+ Delete a SNMP user
"""
if self.use_rest:
- self.delete_snmp_community_rest(current)
+ self.delete_snmp_rest(current)
else:
self.invoke_snmp_community('snmp-community-delete')
- def delete_snmp_community_rest(self, current):
- api = 'support/snmp/users/' + current['engine_id'] + '/' + self.parameters["community_name"]
+ def delete_snmp_rest(self, current):
+ api = 'support/snmp/users/' + current['engine_id'] + '/' + self.parameters["snmp_username"]
dummy, error = self.rest_api.delete(api)
if error:
self.module.fail_json(msg=error)
def apply(self):
- """
- Apply action to SNMP community
- This module is not idempotent:
- Add doesn't fail the playbook if user is trying
- to add an already existing snmp community
- """
- # TODO: This module should of been called snmp_community has it only deals with community and not snmp
+ # TODO: This module should have been called snmp_community as it only deals with community and not snmp
current = self.get_snmp()
cd_action = self.na_helper.get_cd_action(current, self.parameters)
if self.na_helper.changed and not self.module.check_mode:
if cd_action == 'create':
- self.add_snmp_community()
+ self.add_snmp_user()
elif cd_action == 'delete':
- self.delete_snmp_community(current)
+ self.delete_snmp_user(current)
result = netapp_utils.generate_result(self.na_helper.changed, cd_action)
self.module.exit_json(**result)
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp_config.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp_config.py
new file mode 100644
index 000000000..83fed1655
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp_config.py
@@ -0,0 +1,142 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = """
+module: na_ontap_snmp_config
+short_description: NetApp ONTAP module to modify SNMP configuration.
+extends_documentation_fragment:
+ - netapp.ontap.netapp.na_ontap
+version_added: '22.9.0'
+author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
+description:
+ - Modify cluster wide SNMP configuration.
+ - Enable or disable SNMP on a cluster.
+options:
+ state:
+ description:
+ - Modify SNMP configuration, only present is supported.
+ choices: ['present']
+ type: str
+ default: present
+ enabled:
+ description:
+ - Specifies whether to enable or disable SNMP.
+ type: bool
+ required: false
+ auth_traps_enabled:
+ description:
+ - Specifies whether to enable or disable SNMP authentication traps.
+ type: bool
+ required: false
+ traps_enabled:
+ description:
+ - Specifies whether to enable or disable SNMP traps.
+ - Requires ONTAP 9.10.1 or later.
+ type: bool
+ required: false
+
+notes:
+ - Only supported with REST and requires ONTAP 9.7 or later.
+"""
+
+EXAMPLES = """
+ - name: Disable SNMP on cluster
+ netapp.ontap.na_ontap_snmp_config:
+ state: present
+ enabled: false
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+ https: true
+ validate_certs: "{{ validate_certs }}"
+
+ - name: Modify SNMP configuration
+ netapp.ontap.na_ontap_snmp_config:
+ state: present
+ auth_traps_enabled: true
+ traps_enabled: true
+ hostname: "{{ netapp_hostname }}"
+ username: "{{ netapp_username }}"
+ password: "{{ netapp_password }}"
+ https: true
+ validate_certs: "{{ validate_certs }}"
+"""
+
+RETURN = """
+"""
+
+import traceback
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_native
+import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
+from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic
+
+
+class NetAppOntapSNMPConfig:
+ def __init__(self):
+ self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
+ self.argument_spec.update(dict(
+ state=dict(required=False, type='str', choices=['present'], default='present'),
+ enabled=dict(required=False, type='bool'),
+ auth_traps_enabled=dict(required=False, type='bool'),
+ traps_enabled=dict(required=False, type='bool')
+ ))
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ supports_check_mode=True
+ )
+ self.uuid = None
+ self.na_helper = NetAppModule(self.module)
+ self.parameters = self.na_helper.check_and_set_parameters(self.module)
+ self.rest_api = netapp_utils.OntapRestAPI(self.module)
+ self.rest_api.fail_if_not_rest_minimum_version('na_ontap_snmp_config:', 9, 7)
+ self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, None, [['traps_enabled', (9, 10, 1)]])
+
+ def get_snmp_config_rest(self):
+ """Retrieve cluster wide SNMP configuration"""
+ fields = 'enabled'
+ if self.parameters.get('auth_traps_enabled') is not None:
+ fields += ',auth_traps_enabled'
+ if 'traps_enabled' in self.parameters and self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 10, 1):
+ fields += ',traps_enabled'
+ record, error = rest_generic.get_one_record(self.rest_api, 'support/snmp', None, fields)
+ if error:
+ self.module.fail_json(msg="Error fetching SNMP configuration: %s" % to_native(error), exception=traceback.format_exc())
+ if record:
+ return {
+ 'enabled': record.get('enabled'),
+ 'auth_traps_enabled': record.get('auth_traps_enabled'),
+ 'traps_enabled': record.get('traps_enabled')
+ }
+ return None
+
+ def modify_snmp_config_rest(self, modify):
+ """Update cluster wide SNMP configuration"""
+ dummy, error = rest_generic.patch_async(self.rest_api, 'support/snmp', None, modify)
+ if error:
+ self.module.fail_json(msg='Error modifying SNMP configuration: %s.' % to_native(error), exception=traceback.format_exc())
+
+ def apply(self):
+ current = self.get_snmp_config_rest()
+ modify = self.na_helper.get_modified_attributes(current, self.parameters)
+
+ if self.na_helper.changed and not self.module.check_mode:
+ self.modify_snmp_config_rest(modify)
+ result = netapp_utils.generate_result(changed=self.na_helper.changed, modify=modify)
+ self.module.exit_json(**result)
+
+
+def main():
+ snmp_config = NetAppOntapSNMPConfig()
+ snmp_config.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_storage_auto_giveback.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_storage_auto_giveback.py
index 4446371d1..298b07c4b 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_storage_auto_giveback.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_storage_auto_giveback.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2021, NetApp, Inc
+# (c) 2021-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
@@ -128,8 +128,8 @@ class NetAppOntapStorageAutoGiveback(object):
if error is None and records is not None:
return_value = {
'name': message['records'][0]['node'],
- 'auto_giveback_enabled': message['records'][0]['auto_giveback'],
- 'auto_giveback_after_panic_enabled': message['records'][0]['auto_giveback_after_panic']
+ 'auto_giveback_enabled': message['records'][0].get('auto_giveback'),
+ 'auto_giveback_after_panic_enabled': message['records'][0].get('auto_giveback_after_panic')
}
if error:
@@ -228,12 +228,13 @@ class NetAppOntapStorageAutoGiveback(object):
def apply(self):
current = self.get_storage_auto_giveback()
- self.na_helper.get_modified_attributes(current, self.parameters)
+ modify = self.na_helper.get_modified_attributes(current, self.parameters)
if self.na_helper.changed:
if not self.module.check_mode:
self.modify_storage_auto_giveback()
- self.module.exit_json(changed=self.na_helper.changed)
+ result = netapp_utils.generate_result(self.na_helper.changed, modify=modify)
+ self.module.exit_json(**result)
def main():
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_svm.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_svm.py
index 9d5fc6c66..28a10252c 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_svm.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_svm.py
@@ -170,7 +170,7 @@ options:
description:
- If this is set to true, an SVM administrator can manage the NDMP service
- If it is false, only the cluster administrator can manage the service.
- - Requires ONTAP 9.7 or later.
+ - Requires ONTAP 9.10.1 or later.
type: bool
version_added: 21.24.0
aggr_list:
@@ -469,9 +469,9 @@ class NetAppOntapSVM():
]
if errors:
self.module.fail_json(msg='Error - %s' % ' '.join(errors))
- if use_rest and self.parameters.get('services') and not self.parameters.get('allowed_protocols') and self.parameters['services'].get('ndmp')\
- and not self.rest_api.meets_rest_minimum_version(use_rest, 9, 7):
- self.module.fail_json(msg=self.rest_api.options_require_ontap_version('ndmp', '9.7', use_rest=use_rest))
+ if use_rest and self.parameters.get('services') and not self.parameters.get('allowed_protocols'):
+ if self.parameters['services'].get('ndmp') and not self.rest_api.meets_rest_minimum_version(use_rest, 9, 10, 1):
+ self.module.fail_json(msg=self.rest_api.options_require_ontap_version('ndmp', '9.10.1', use_rest=use_rest))
if self.parameters.get('services') and not use_rest:
self.module.fail_json(msg=self.rest_api.options_require_ontap_version('services', use_rest=use_rest))
if self.parameters.get('web'):
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_user.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_user.py
index 7fada8ac6..e95ab492a 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_user.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_user.py
@@ -654,7 +654,8 @@ class NetAppOntapUser:
error = self.patch_account(owner_uuid, username, body)
if error:
if 'message' in error and self.is_repeated_password(error['message']):
- # if the password is reused, assume idempotency
+ # if the password is reused, assume idempotency but show a warning
+ self.module.warn('Password was not changed: %s' % error['message'])
return False
self.module.fail_json(msg='Error while updating user password: %s' % error)
return True
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_volume.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_volume.py
index 7ca007c29..8f5d827e7 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_volume.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_volume.py
@@ -4,17 +4,11 @@
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-'''
-na_ontap_volume
-'''
-
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
-
module: na_ontap_volume
-
short_description: NetApp ONTAP manage volumes.
extends_documentation_fragment:
- netapp.ontap.netapp.na_ontap
@@ -307,6 +301,7 @@ options:
- This is an advanced option, the default is False.
- Enable the visible '.snapshot' directory that is normally present at system internal mount points.
- This value also turns on access to all other '.snapshot' directories in the volume.
+ - This option is supported in REST for ONTAP 9.13.1 or later with ONTAP collection version 22.8.0 or later.
type: bool
version_added: 2.8.0
@@ -318,9 +313,28 @@ options:
since it prevents writes to the inode file for the volume from contending with reads from other files.
- This field should be used carefully.
- That is, use this field when you know in advance that the correct access time for inodes will not be needed for files on that volume.
+ - This option is supported in REST for ONTAP 9.8 or later with ONTAP collection version 22.8.0 or later.
type: bool
version_added: 2.8.0
+ vol_nearly_full_threshold_percent:
+ description:
+ - Specifies the percentage at which the volume is considered nearly full, and above which an EMS warning will be generated.
+ - The default value is 95%. The maximum value for this option is 99%.
+ - Setting this threshold to 0 disables the volume nearly full space alerts.
+ - Supported only with in REST for ONTAP 9.9 or later.
+ type: int
+ version_added: 22.8.0
+
+ vol_full_threshold_percent:
+ description:
+ - Specifies the percentage at which the volume is considered full, and above which a critical EMS error will be generated.
+ - The default value is 98%. The maximum value for this option is 100%.
+ - Setting this threshold to 0 disables the volume full space alerts.
+ - Supported only with in REST for ONTAP 9.9 or later.
+ type: int
+ version_added: 22.8.0
+
wait_for_completion:
description:
- Set this parameter to 'true' for synchronous execution during create (wait until volume status is online)
@@ -346,7 +360,7 @@ options:
description:
- Volume move and encryption operations might take longer time to complete.
- With C(wait_for_completion) set, module will wait for time set in this option for volume move and encryption to complete.
- - If time exipres, module exit and the operation may still running.
+ - If time exipres, module exit and the operation may still be running.
- Default is set to 10 minutes.
default: 600
type: int
@@ -459,6 +473,7 @@ options:
- A dictionary for the auto delete options and values.
- Supported options include 'state', 'commitment', 'trigger', 'target_free_space', 'delete_order', 'defer_delete',
'prefix', 'destroy_list'.
+ - All the above mentioned options except 'destroy_list' are supported in REST for ONTAP 9.13.1 or later with ONTAP collection version 22.8.0 or later.
- Option 'state' determines if the snapshot autodelete is currently enabled for the volume. Possible values are 'on' and 'off'.
- Option 'commitment' determines the snapshots which snapshot autodelete is allowed to delete to get back space.
Possible values are 'try', 'disrupt' and 'destroy'.
@@ -880,6 +895,33 @@ EXAMPLES = """
retention:
default: "{{ 60 | netapp.ontap.iso8601_duration_from_seconds }}"
+ - name: Create volume with snapshot-auto-delete options - REST
+ netapp.ontap.na_ontap_volume:
+ state: present
+ name: test_vol
+ aggregate_name: "{{ aggr }}"
+ size: 20
+ size_unit: mb
+ snapshot_auto_delete:
+ state: 'on'
+ trigger: volume
+ delete_order: "oldest_first"
+ defer_delete: "user_created"
+ commitment: "try"
+ target_free_space: 30
+ prefix: "my_prefix"
+ wait_for_completion: true
+
+ - name: Modify volume - REST
+ netapp.ontap.na_ontap_volume:
+ state: present
+ name: test_vol
+ aggregate_name: "{{ aggr }}"
+ snapdir_access: false
+ snapshot_auto_delete:
+ state: 'on'
+ target_free_space: 25
+
"""
RETURN = """
@@ -929,6 +971,8 @@ class NetAppOntapVolume:
aggr_list_multiplier=dict(required=False, type='int'),
snapdir_access=dict(required=False, type='bool'),
atime_update=dict(required=False, type='bool'),
+ vol_nearly_full_threshold_percent=dict(required=False, type='int'),
+ vol_full_threshold_percent=dict(required=False, type='int'),
auto_provision_as=dict(choices=['flexgroup'], required=False, type='str'),
wait_for_completion=dict(required=False, type='bool', default=False),
time_out=dict(required=False, type='int', default=180),
@@ -1021,19 +1065,20 @@ class NetAppOntapVolume:
netapp_utils.POW2_BYTE_MAP[self.parameters['size_unit']]
self.validate_snapshot_auto_delete()
self.rest_api = netapp_utils.OntapRestAPI(self.module)
- unsupported_rest_properties = ['atime_update',
- 'cutover_action',
+ unsupported_rest_properties = ['cutover_action',
'encrypt-destination',
'force_restore',
'nvfail_enabled',
'preserve_lun_ids',
- 'snapdir_access',
- 'snapshot_auto_delete',
+ 'destroy_list',
'space_slo',
'vserver_dr_protection']
- partially_supported_rest_properties = [['efficiency_policy', (9, 7)], ['tiering_minimum_cooling_days', (9, 8)], ['analytics', (9, 8)],
- ['tags', (9, 13, 1)]]
- self.unsupported_zapi_properties = ['sizing_method', 'logical_space_enforcement', 'logical_space_reporting', 'snaplock', 'analytics', 'tags']
+ partially_supported_rest_properties = [['efficiency_policy', (9, 7)], ['tiering_minimum_cooling_days', (9, 8)],
+ ['analytics', (9, 8)], ['atime_update', (9, 8)],
+ ['vol_nearly_full_threshold_percent', (9, 9)], ['vol_full_threshold_percent', (9, 9)],
+ ['tags', (9, 13, 1)], ['snapdir_access', (9, 13, 1)], ['snapshot_auto_delete', (9, 13, 1)]]
+ self.unsupported_zapi_properties = ['sizing_method', 'logical_space_enforcement', 'logical_space_reporting', 'snaplock',
+ 'analytics', 'tags', 'vol_nearly_full_threshold_percent', 'vol_full_threshold_percent']
self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, unsupported_rest_properties, partially_supported_rest_properties)
if not self.use_rest:
@@ -1313,6 +1358,25 @@ class NetAppOntapVolume:
self.na_helper.fail_on_error(error)
return response
+ def wait_for_volume_online(self, sleep_time=10):
+ # round off time_out
+ retries = (self.parameters['time_out'] + 5) // 10
+ is_online = None
+ errors = []
+ while not is_online and retries > 0:
+ try:
+ current = self.get_volume()
+ is_online = None if current is None else current['is_online']
+ except KeyError as err:
+ # get_volume may receive incomplete data as the volume is being created
+ errors.append(repr(err))
+ if not is_online:
+ time.sleep(sleep_time)
+ retries -= 1
+ if not is_online:
+ errors.append("Timeout after %s seconds" % self.parameters['time_out'])
+ self.module.fail_json(msg='Error waiting for volume %s to come online: %s' % (self.parameters['name'], str(errors)))
+
def create_volume(self):
'''Create ONTAP volume'''
if self.rest_app:
@@ -1333,24 +1397,7 @@ class NetAppOntapVolume:
exception=traceback.format_exc())
if self.parameters.get('wait_for_completion'):
- # round off time_out
- retries = (self.parameters['time_out'] + 5) // 10
- is_online = None
- errors = []
- while not is_online and retries > 0:
- try:
- current = self.get_volume()
- is_online = None if current is None else current['is_online']
- except KeyError as err:
- # get_volume may receive incomplete data as the volume is being created
- errors.append(repr(err))
- if not is_online:
- time.sleep(10)
- retries -= 1
- if not is_online:
- errors.append("Timeout after %s seconds" % self.parameters['time_out'])
- self.module.fail_json(msg='Error waiting for volume %s to come online: %s'
- % (self.parameters['name'], str(errors)))
+ self.wait_for_volume_online()
return None
def create_volume_async(self):
@@ -1948,12 +1995,13 @@ class NetAppOntapVolume:
'snapshot_policy', 'percent_snapshot_space', 'snapdir_access', 'atime_update', 'volume_security_style',
'nvfail_enabled', 'space_slo', 'qos_policy_group', 'qos_adaptive_policy_group', 'vserver_dr_protection',
'comment', 'logical_space_enforcement', 'logical_space_reporting', 'tiering_minimum_cooling_days',
- 'snaplock', 'max_files', 'analytics', 'tags']:
+ 'snaplock', 'max_files', 'analytics', 'tags', 'snapshot_auto_delete', 'vol_nearly_full_threshold_percent',
+ 'vol_full_threshold_percent']:
self.volume_modify_attributes(modify)
break
if 'snapshot_auto_delete' in attributes and not self.use_rest:
- # Rest doesn't support any snapshot_auto_delete option other than is_autodelete_enabled. For now i've completely
- # disabled this in rest
+ # Rest didn't support snapshot_auto_delete prior to ONTAP 9.13.1; for supported ONTAP versions,
+ # modification for this parameter is handled by calling volume_modify_attributes function.
self.set_snapshot_auto_delete()
# don't mount or unmount when offline
if modify.get('junction_path'):
@@ -2265,6 +2313,8 @@ class NetAppOntapVolume:
auto_delete_info = current.pop('snapshot_auto_delete', None)
# ignore small changes in volume size or inode maximum by adjusting self.parameters['size'] or self.parameters['max_files']
self.adjust_sizes(current, after_create)
+ if 'type' in self.parameters:
+ self.parameters['type'] = self.parameters['type'].lower()
modify = self.na_helper.get_modified_attributes(current, self.parameters)
if modify is not None and 'type' in modify:
msg = "Error: volume type was not set properly at creation time." if after_create else \
@@ -2393,6 +2443,16 @@ class NetAppOntapVolume:
params['fields'] += 'analytics,'
if self.parameters.get('tags'):
params['fields'] += '_tags,'
+ if self.parameters.get('atime_update') is not None:
+ params['fields'] += 'access_time_enabled,'
+ if self.parameters.get('snapdir_access') is not None:
+ params['fields'] += 'snapshot_directory_access_enabled,'
+ if self.parameters.get('snapshot_auto_delete') is not None:
+ params['fields'] += 'space.snapshot.autodelete,'
+ if self.parameters.get('vol_nearly_full_threshold_percent') is not None:
+ params['fields'] += 'space.nearly_full_threshold_percent,'
+ if self.parameters.get('vol_full_threshold_percent') is not None:
+ params['fields'] += 'space.full_threshold_percent,'
record, error = rest_generic.get_one_record(self.rest_api, api, params)
if error:
@@ -2432,6 +2492,8 @@ class NetAppOntapVolume:
if error:
self.module.fail_json(msg='Error creating volume %s: %s' % (self.parameters['name'], to_native(error)),
exception=traceback.format_exc())
+ if self.parameters.get('wait_for_completion'):
+ self.wait_for_volume_online(sleep_time=5)
def create_volume_body_rest(self):
body = {
@@ -2464,7 +2526,7 @@ class NetAppOntapVolume:
if self.parameters.get('comment') is not None:
body['comment'] = self.parameters['comment']
if self.parameters.get('type') is not None:
- body['type'] = self.parameters['type']
+ body['type'] = self.parameters['type'].lower()
if self.parameters.get('percent_snapshot_space') is not None:
body['space.snapshot.reserve_percent'] = self.parameters['percent_snapshot_space']
if self.parameters.get('language') is not None:
@@ -2514,6 +2576,13 @@ class NetAppOntapVolume:
def bool_to_online(item):
return 'online' if item else 'offline'
+ @staticmethod
+ def enabled_to_bool(item, reverse=False):
+ """ convertes on/off to true/false or vice versa """
+ if reverse:
+ return 'on' if item else 'off'
+ return True if item == 'on' else False
+
def modify_volume_body_rest(self, params):
body = {}
for key, option, transform in [
@@ -2533,7 +2602,11 @@ class NetAppOntapVolume:
('space.logical_space.reporting', 'logical_space_reporting', None),
('tiering.min_cooling_days', 'tiering_minimum_cooling_days', None),
('state', 'is_online', self.bool_to_online),
- ('_tags', 'tags', None)
+ ('_tags', 'tags', None),
+ ('snapshot_directory_access_enabled', 'snapdir_access', None),
+ ('access_time_enabled', 'atime_update', None),
+ ('space.nearly_full_threshold_percent', 'vol_nearly_full_threshold_percent', None),
+ ('space.full_threshold_percent', 'vol_full_threshold_percent', None),
]:
value = self.parameters.get(option)
if value is not None and transform:
@@ -2557,6 +2630,22 @@ class NetAppOntapVolume:
sl_dict.pop('type', None)
if sl_dict:
body['snaplock'] = sl_dict
+
+ if params and params.get('snapshot_auto_delete') is not None:
+ for key, option, transform in [
+ ('space.snapshot.autodelete.trigger', 'trigger', None),
+ ('space.snapshot.autodelete.target_free_space', 'target_free_space', None),
+ ('space.snapshot.autodelete.delete_order', 'delete_order', None),
+ ('space.snapshot.autodelete.commitment', 'commitment', None),
+ ('space.snapshot.autodelete.defer_delete', 'defer_delete', None),
+ ('space.snapshot.autodelete.prefix', 'prefix', None),
+ ('space.snapshot.autodelete.enabled', 'state', self.enabled_to_bool),
+ ]:
+ if params and params['snapshot_auto_delete'].get(option) is not None:
+ if transform:
+ body[key] = transform(self.parameters['snapshot_auto_delete'][option])
+ else:
+ body[key] = self.parameters['snapshot_auto_delete'][option]
return body
def change_volume_state_rest(self):
@@ -2683,6 +2772,10 @@ class NetAppOntapVolume:
self.na_helper.safe_get(self.parameters, ['nas_application_template', 'flexcache', 'dr_cache']) is not None:
self.module.fail_json(msg='Error: %s' % self.rest_api.options_require_ontap_version('flexcache: dr_cache', version='9.9'))
+ if 'snapshot_auto_delete' in self.parameters:
+ if 'destroy_list' in self.parameters['snapshot_auto_delete']:
+ self.module.fail_json(msg="snapshot_auto_delete option 'destroy_list' is currently not supported with REST.")
+
def format_get_volume_rest(self, record):
is_online = record.get('state') == 'online'
# TODO FIX THIS!!!! ZAPI would only return a single aggr, REST can return more than 1.
@@ -2696,6 +2789,10 @@ class NetAppOntapVolume:
# if analytics.state is initializing it will be ON once completed.
state = self.na_helper.safe_get(record, ['analytics', 'state'])
analytics = 'on' if state == 'initializing' else state
+ auto_delete_info = self.na_helper.safe_get(record, ['space', 'snapshot', 'autodelete'])
+ if auto_delete_info is not None:
+ auto_delete_info['state'] = self.enabled_to_bool(self.na_helper.safe_get(record, ['space', 'snapshot', 'autodelete', 'enabled']), reverse=True)
+ del auto_delete_info['enabled']
return {
'tags': record.get('_tags', []),
'name': record.get('name', None),
@@ -2732,7 +2829,12 @@ class NetAppOntapVolume:
'tiering_minimum_cooling_days': self.na_helper.safe_get(record, ['tiering', 'min_cooling_days']),
'snaplock': self.na_helper.safe_get(record, ['snaplock']),
'max_files': self.na_helper.safe_get(record, ['files', 'maximum']),
-
+ # The default setting for access_time_enabled and snapshot_directory_access_enabled is true
+ 'atime_update': record.get('access_time_enabled', True),
+ 'snapdir_access': record.get('snapshot_directory_access_enabled', True),
+ 'snapshot_auto_delete': auto_delete_info,
+ 'vol_nearly_full_threshold_percent': self.na_helper.safe_get(record, ['space', 'nearly_full_threshold_percent']),
+ 'vol_full_threshold_percent': self.na_helper.safe_get(record, ['space', 'full_threshold_percent']),
}
def is_fabricpool(self, name, aggregate_uuid):
@@ -2868,6 +2970,8 @@ class NetAppOntapVolume:
# if we create using ZAPI and modify only options are set (snapdir_access or atime_update), we need to run a modify.
# The modify also takes care of efficiency (sis) parameters and snapshot_auto_delete.
# If we create using REST application, some options are not available, we may need to run a modify.
+ # If we create using REST and modify only options are set (snapdir_access or atime_update or snapshot_auto_delete), we need to run a modify.
+ # For modify only options to be set after creation wait_for_completion needs to be set.
# volume should be online for modify.
current = self.get_volume()
if current:
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vscan_scanner_pool.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vscan_scanner_pool.py
index 20e480637..831ae7253 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vscan_scanner_pool.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vscan_scanner_pool.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2018-2019, NetApp, Inc
+# (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
@@ -108,6 +108,8 @@ from ansible.module_utils._text import to_native
import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
from ansible_collections.netapp.ontap.plugins.module_utils.netapp import OntapRestAPI
from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic
+from ansible_collections.netapp.ontap.plugins.module_utils import rest_vserver
HAS_NETAPP_LIB = netapp_utils.has_netapp_lib()
@@ -129,13 +131,18 @@ class NetAppOntapVscanScannerPool(object):
argument_spec=self.argument_spec,
supports_check_mode=True
)
+ self.svm_uuid = None
self.na_helper = NetAppModule()
self.parameters = self.na_helper.set_parameters(self.module.params)
self.rest_api = OntapRestAPI(self.module)
- if HAS_NETAPP_LIB is False:
- self.module.fail_json(msg="the python NetApp-Lib module is required")
- else:
+ self.use_rest = self.rest_api.is_rest()
+ if self.use_rest and not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 6):
+ msg = 'REST requires ONTAP 9.6 or later for /protocols/vscan/{{svm.uuid}}/scanner-pools APIs'
+ self.use_rest = self.na_helper.fall_back_to_zapi(self.module, msg, self.parameters)
+ if not self.use_rest:
+ if HAS_NETAPP_LIB is False:
+ self.module.fail_json(msg=netapp_utils.netapp_lib_is_required())
self.server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.parameters['vserver'])
def create_scanner_pool(self):
@@ -143,6 +150,8 @@ class NetAppOntapVscanScannerPool(object):
Create a Vscan Scanner Pool
:return: nothing
"""
+ if self.use_rest:
+ return self.create_scanner_pool_rest()
scanner_pool_obj = netapp_utils.zapi.NaElement('vscan-scanner-pool-create')
if self.parameters['hostnames']:
string_obj = netapp_utils.zapi.NaElement('hostnames')
@@ -182,37 +191,36 @@ class NetAppOntapVscanScannerPool(object):
Check to see if a scanner pool exist or not
:return: True if it exist, False if it does not
"""
- return_value = None
if self.use_rest:
- pass
- else:
- scanner_pool_obj = netapp_utils.zapi.NaElement('vscan-scanner-pool-get-iter')
- scanner_pool_info = netapp_utils.zapi.NaElement('vscan-scanner-pool-info')
- scanner_pool_info.add_new_child('scanner-pool', self.parameters['scanner_pool'])
- scanner_pool_info.add_new_child('vserver', self.parameters['vserver'])
- query = netapp_utils.zapi.NaElement('query')
- query.add_child_elem(scanner_pool_info)
- scanner_pool_obj.add_child_elem(query)
- try:
- result = self.server.invoke_successfully(scanner_pool_obj, True)
- except netapp_utils.zapi.NaApiError as error:
- self.module.fail_json(msg='Error searching for Vscan Scanner Pool %s: %s' %
- (self.parameters['scanner_pool'], to_native(error)), exception=traceback.format_exc())
- if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1:
- if result.get_child_by_name('attributes-list').get_child_by_name('vscan-scanner-pool-info').get_child_content(
- 'scanner-pool') == self.parameters['scanner_pool']:
- scanner_pool_obj = result.get_child_by_name('attributes-list').get_child_by_name('vscan-scanner-pool-info')
- hostname = [host.get_content() for host in
- scanner_pool_obj.get_child_by_name('hostnames').get_children()]
- privileged_users = [user.get_content() for user in
- scanner_pool_obj.get_child_by_name('privileged-users').get_children()]
- return_value = {
- 'hostnames': hostname,
- 'enable': scanner_pool_obj.get_child_content('is-currently-active'),
- 'privileged_users': privileged_users,
- 'scanner_pool': scanner_pool_obj.get_child_content('scanner-pool'),
- 'scanner_policy': scanner_pool_obj.get_child_content('scanner-policy')
- }
+ return self.get_scanner_pool_rest()
+ return_value = None
+ scanner_pool_obj = netapp_utils.zapi.NaElement('vscan-scanner-pool-get-iter')
+ scanner_pool_info = netapp_utils.zapi.NaElement('vscan-scanner-pool-info')
+ scanner_pool_info.add_new_child('scanner-pool', self.parameters['scanner_pool'])
+ scanner_pool_info.add_new_child('vserver', self.parameters['vserver'])
+ query = netapp_utils.zapi.NaElement('query')
+ query.add_child_elem(scanner_pool_info)
+ scanner_pool_obj.add_child_elem(query)
+ try:
+ result = self.server.invoke_successfully(scanner_pool_obj, True)
+ except netapp_utils.zapi.NaApiError as error:
+ self.module.fail_json(msg='Error searching for Vscan Scanner Pool %s: %s' %
+ (self.parameters['scanner_pool'], to_native(error)), exception=traceback.format_exc())
+ if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1:
+ if result.get_child_by_name('attributes-list').get_child_by_name('vscan-scanner-pool-info').get_child_content(
+ 'scanner-pool') == self.parameters['scanner_pool']:
+ scanner_pool_obj = result.get_child_by_name('attributes-list').get_child_by_name('vscan-scanner-pool-info')
+ hostname = [host.get_content() for host in
+ scanner_pool_obj.get_child_by_name('hostnames').get_children()]
+ privileged_users = [user.get_content() for user in
+ scanner_pool_obj.get_child_by_name('privileged-users').get_children()]
+ return_value = {
+ 'hostnames': hostname,
+ 'enable': scanner_pool_obj.get_child_content('is-currently-active'),
+ 'privileged_users': privileged_users,
+ 'scanner_pool': scanner_pool_obj.get_child_content('scanner-pool'),
+ 'scanner_policy': scanner_pool_obj.get_child_content('scanner-policy')
+ }
return return_value
def delete_scanner_pool(self):
@@ -220,6 +228,8 @@ class NetAppOntapVscanScannerPool(object):
Delete a Scanner pool
:return: nothing
"""
+ if self.use_rest:
+ return self.delete_scanner_pool_rest()
scanner_pool_obj = netapp_utils.zapi.NaElement('vscan-scanner-pool-delete')
scanner_pool_obj.add_new_child('scanner-pool', self.parameters['scanner_pool'])
try:
@@ -234,6 +244,8 @@ class NetAppOntapVscanScannerPool(object):
Modify a scanner pool
:return: nothing
"""
+ if self.use_rest:
+ return self.modify_scanner_pool_rest(modify)
vscan_pool_modify = netapp_utils.zapi.NaElement('vscan-scanner-pool-modify')
vscan_pool_modify.add_new_child('scanner-pool', self.parameters['scanner_pool'])
for key in modify:
@@ -261,26 +273,117 @@ class NetAppOntapVscanScannerPool(object):
def attribute_to_name(attribute):
return str.replace(attribute, '_', '-')
+ def get_svm_uuid(self):
+ """
+ Get a vserver's uuid
+ :return: nothing
+ """
+ record, error = rest_vserver.get_vserver_uuid(self.rest_api, self.parameters['vserver'])
+ if error is not None:
+ self.module.fail_json(msg="Error fetching vserver %s: %s" % (self.parameters['vserver'], to_native(error)),
+ exception=traceback.format_exc())
+ if record is None:
+ self.module.fail_json(msg="Error fetching vserver %s. Please make sure vserver name is correct."
+ % self.parameters['vserver'], exception=traceback.format_exc())
+ self.svm_uuid = record
+
+ def get_scanner_pool_rest(self):
+ """
+ Check to see if a scanner pool exist or not using REST
+ :return: record if it exist, None if it does not
+ """
+ self.get_svm_uuid()
+ api = 'protocols/vscan/%s/scanner-pools' % self.svm_uuid
+ query = {'name': self.parameters.get('scanner_pool'),
+ 'fields': 'servers,'
+ 'privileged_users,'}
+ if self.parameters.get('scanner_policy') is not None:
+ query['fields'] += 'role,'
+
+ record, error = rest_generic.get_one_record(self.rest_api, api, query)
+ if error:
+ self.module.fail_json(msg='Error searching for Vscan Scanner Pool %s: %s' %
+ (self.parameters['scanner_pool'], to_native(error)),
+ exception=traceback.format_exc())
+ if record:
+ return {
+ 'scanner_pool': record.get('name'),
+ 'hostnames': record.get('servers'),
+ 'privileged_users': record.get('privileged_users'),
+ 'scanner_policy': record.get('role'),
+ }
+ return None
+
+ def create_scanner_pool_rest(self):
+ """
+ Create a Vscan Scanner Pool using REST
+ :return: nothing
+ """
+ api = 'protocols/vscan/%s/scanner-pools' % self.svm_uuid
+ body = {
+ 'name': self.parameters['scanner_pool'],
+ 'servers': self.parameters['hostnames'],
+ 'privileged_users': self.parameters['privileged_users'],
+ }
+ if 'scanner_policy' in self.parameters:
+ body['role'] = self.parameters['scanner_policy']
+
+ dummy, error = rest_generic.post_async(self.rest_api, api, body)
+ if error is not None:
+ self.module.fail_json(msg='Error creating Vscan Scanner Pool %s: %s' %
+ (self.parameters['scanner_pool'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def delete_scanner_pool_rest(self):
+ """
+ Delete a Scanner pool using REST
+ :return: nothing
+ """
+ api = 'protocols/vscan/%s/scanner-pools/%s' % (self.svm_uuid, self.parameters['scanner_pool'])
+ dummy, error = rest_generic.delete_async(self.rest_api, api, uuid=None)
+ if error is not None:
+ self.module.fail_json(msg='Error deleting Vscan Scanner Pool %s: %s' %
+ (self.parameters['scanner_pool'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def modify_scanner_pool_rest(self, modify):
+ """
+ Modify a scanner pool using REST
+ :return: nothing
+ """
+ api = 'protocols/vscan/%s/scanner-pools/%s' % (self.svm_uuid, self.parameters['scanner_pool'])
+ body = {}
+ for key, option in [
+ ('servers', 'hostnames'),
+ ('privileged_users', 'privileged_users'),
+ ('role', 'scanner_policy'),
+ ]:
+ if modify.get(option) is not None:
+ body[key] = modify[option]
+
+ dummy, error = rest_generic.patch_async(self.rest_api, api, uuid_or_name=None, body=body)
+ if error:
+ self.module.fail_json(msg='Error modifying Vscan Scanner Pool %s: %s.' %
+ (self.parameters['scanner_pool'], to_native(error)),
+ exception=traceback.format_exc())
+
def apply(self):
- scanner_pool_obj = self.get_scanner_pool()
- cd_action = self.na_helper.get_cd_action(scanner_pool_obj, self.parameters)
+ current = self.get_scanner_pool()
+ cd_action = self.na_helper.get_cd_action(current, self.parameters)
modify = None
if self.parameters['state'] == 'present' and cd_action is None:
- modify = self.na_helper.get_modified_attributes(scanner_pool_obj, self.parameters)
- if self.na_helper.changed:
- if self.module.check_mode:
- pass
- else:
- if cd_action == 'create':
- self.create_scanner_pool()
- if self.parameters.get('scanner_policy') is not None:
- self.apply_policy()
- elif cd_action == 'delete':
- self.delete_scanner_pool()
- elif modify:
- self.modify_scanner_pool(modify)
- if self.parameters.get('scanner_policy') is not None:
- self.apply_policy()
+ modify = self.na_helper.get_modified_attributes(current, self.parameters)
+ if self.na_helper.changed and not self.module.check_mode:
+ if cd_action == 'create':
+ self.create_scanner_pool()
+ if not self.use_rest and self.parameters.get('scanner_policy') is not None:
+ self.apply_policy()
+ elif cd_action == 'delete':
+ self.delete_scanner_pool()
+ elif modify:
+ self.modify_scanner_pool(modify)
+ if not self.use_rest and self.parameters.get('scanner_policy') is not None:
+ self.apply_policy()
result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify)
self.module.exit_json(**result)
@@ -289,8 +392,8 @@ def main():
"""
Execute action from playbook
"""
- command = NetAppOntapVscanScannerPool()
- command.apply()
+ scanner_pool = NetAppOntapVscanScannerPool()
+ scanner_pool.apply()
if __name__ == '__main__':
diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vserver_peer.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vserver_peer.py
index 3c34ccf08..5f7c7260d 100644
--- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vserver_peer.py
+++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vserver_peer.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
-# (c) 2018-2022, NetApp, Inc
+# (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
@@ -185,7 +185,9 @@ class NetAppONTAPVserverPeer:
self.dst_rest_api = OntapRestAPI(self.module, host_options=self.parameters['peer_options'])
self.dst_use_rest = self.dst_rest_api.is_rest()
self.use_rest = bool(self.src_use_rest and self.dst_use_rest)
- if not self.use_rest:
+ if self.use_rest:
+ self.peer_relation_uuid = None
+ else:
if not netapp_utils.has_netapp_lib():
self.module.fail_json(msg=netapp_utils.netapp_lib_is_required())
self.server = netapp_utils.setup_na_ontap_zapi(module=self.module)
@@ -365,6 +367,10 @@ class NetAppONTAPVserverPeer:
vserver, remote_vserver = self.get_local_and_peer_vserver(target)
restapi = self.rest_api if target == 'source' else self.dst_rest_api
options = {'svm.name': vserver, 'peer.svm.name': remote_vserver, 'fields': 'name,svm.name,peer.svm.name,state,uuid'}
+ # peer cluster may have multiple peer relationships
+ # filter by the created relationship uuid
+ if target == 'peer' and self.peer_relation_uuid is not None:
+ options['uuid'] = self.peer_relation_uuid
record, error = rest_generic.get_one_record(restapi, api, options)
if error:
self.module.fail_json(msg='Error fetching vserver peer %s: %s' % (self.parameters['vserver'], error))
@@ -407,16 +413,19 @@ class NetAppONTAPVserverPeer:
Create a vserver peer using rest
"""
api = 'svm/peers'
- params = {
+ query = {'return_records': 'true'}
+ body = {
'svm.name': self.parameters['vserver'],
'peer.cluster.name': self.parameters['peer_cluster'],
'peer.svm.name': self.parameters['peer_vserver'],
'applications': self.parameters['applications']
}
if 'local_name_for_peer' in self.parameters:
- params['name'] = self.parameters['local_name_for_peer']
- dummy, error = rest_generic.post_async(self.rest_api, api, params)
+ body['name'] = self.parameters['local_name_for_peer']
+ record, error = rest_generic.post_async(self.rest_api, api, body, query)
self.check_and_report_rest_error(error, 'creating', self.parameters['vserver'])
+ if record.get('records') is not None:
+ self.peer_relation_uuid = record['records'][0].get('uuid')
def apply(self):
"""