From ff91db3ecec454ff569187084ec04e45dff7fb21 Mon Sep 17 00:00:00 2001
From: Douglas Viroel <viroel@gmail.com>
Date: Fri, 22 Jan 2021 16:59:48 -0300
Subject: [PATCH] [NetApp] Implement security service update

This patch implements support for security service updates
for in use share networks. It works with all three security
service types. For 'active_directory' and 'kerberos', the 'domain'
attribute update isn't supported, since it can might affect
user's access to all related shares.

Change-Id: I8556e4e2e05deb9b116eacbd5afe2f7c5d77b44b
Depends-On: I129a794dfd2d179fa2b9a2fed050459d6f00b0de
Depends-On: I5fef50a17bc72ba66a3a9d6f786742bcb5745d7b
Implements: bp netapp-security-service-update
Co-Authored-By: Carlos Eduardo <ces.eduardo98@gmail.com>
Signed-off-by: Douglas Viroel <viroel@gmail.com>
---
 manila/message/message_field.py               |  10 +
 .../netapp/dataontap/client/client_cmode.py   | 162 +++++++++++++-
 .../dataontap/cluster_mode/drv_multi_svm.py   |  19 ++
 .../dataontap/cluster_mode/drv_single_svm.py  |  12 ++
 .../netapp/dataontap/cluster_mode/lib_base.py |   3 +
 .../dataontap/cluster_mode/lib_multi_svm.py   | 155 ++++++++++++++
 .../drivers/netapp/dataontap/client/fakes.py  |  13 +-
 .../dataontap/client/test_client_cmode.py     | 200 ++++++++++++++++++
 .../cluster_mode/test_lib_multi_svm.py        | 161 ++++++++++++++
 .../share/drivers/netapp/dataontap/fakes.py   |  59 +++++-
 ...urity-service-update-718a68ebe60fd2b5.yaml |  10 +
 11 files changed, 797 insertions(+), 7 deletions(-)
 create mode 100644 releasenotes/notes/netapp-add-security-service-update-718a68ebe60fd2b5.yaml

diff --git a/manila/message/message_field.py b/manila/message/message_field.py
index b96e1bfaf4..f9fa56de8e 100644
--- a/manila/message/message_field.py
+++ b/manila/message/message_field.py
@@ -20,6 +20,7 @@ class Resource(object):
     SHARE_GROUP = 'SHARE_GROUP'
     SHARE_REPLICA = 'SHARE_REPLICA'
     SHARE_SNAPSHOT = 'SHARE_SNAPSHOT'
+    SECURITY_SERVICE = 'SECURITY_SERVICE'
 
 
 class Action(object):
@@ -34,6 +35,7 @@ class Action(object):
     EXTEND = ('008', _('extend'))
     SHRINK = ('009', _('shrink'))
     UPDATE_ACCESS_RULES = ('010', _('update access rules'))
+    ADD_UPDATE_SECURITY_SERVICE = ('011', _('add or update security service'))
     ALL = (
         ALLOCATE_HOST,
         CREATE,
@@ -45,6 +47,7 @@ class Action(object):
         EXTEND,
         SHRINK,
         UPDATE_ACCESS_RULES,
+        ADD_UPDATE_SECURITY_SERVICE,
     )
 
 
@@ -111,6 +114,12 @@ class Detail(object):
         _("Failed to grant access to client. The access level or type may "
           "be unsupported. You may try again with a different access level "
           "or access type."))
+    UNSUPPORTED_ADD_UDPATE_SECURITY_SERVICE = (
+        '022',
+        _("Share driver has failed to setup one or more security services "
+          "that are associated with the used share network. The security "
+          "service may be unsupported or the provided parameters are invalid. "
+          "You may try again with a different set of configurations."))
 
     ALL = (
         UNKNOWN_ERROR,
@@ -134,6 +143,7 @@ class Detail(object):
         DRIVER_FAILED_SHRINK,
         FORBIDDEN_CLIENT_ACCESS,
         UNSUPPORTED_CLIENT_ACCESS,
+        UNSUPPORTED_ADD_UDPATE_SECURITY_SERVICE,
     )
 
     # Exception and detail mappings
diff --git a/manila/share/drivers/netapp/dataontap/client/client_cmode.py b/manila/share/drivers/netapp/dataontap/client/client_cmode.py
index 2799a4427f..af19b14e77 100644
--- a/manila/share/drivers/netapp/dataontap/client/client_cmode.py
+++ b/manila/share/drivers/netapp/dataontap/client/client_cmode.py
@@ -1535,7 +1535,6 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
             self.configure_dns(security_service)
 
         config_name = hashlib.md5(six.b(security_service['id'])).hexdigest()
-
         api_args = {
             'ldap-client-config': config_name,
             'tcp-port': '389',
@@ -1590,6 +1589,13 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
             LOG.exception(msg)
             raise exception.NetAppException(message=msg)
 
+    @na_utils.trace
+    def _delete_ldap_client(self, security_service):
+        config_name = (
+            hashlib.md5(six.b(security_service['id'])).hexdigest())
+        api_args = {'ldap-client-config': config_name}
+        self.send_request('ldap-client-delete', api_args)
+
     @na_utils.trace
     def configure_ldap(self, security_service, timeout=30):
         """Configures LDAP on Vserver."""
@@ -1598,17 +1604,59 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
         self._enable_ldap_client(config_name, timeout=timeout)
 
     @na_utils.trace
-    def configure_active_directory(self, security_service, vserver_name):
-        """Configures AD on Vserver."""
-        self.configure_dns(security_service)
-        self.set_preferred_dc(security_service)
+    def modify_ldap(self, new_security_service, current_security_service):
+        """Modifies LDAP client on a Vserver."""
+        # Create a new ldap client
+        self._create_ldap_client(new_security_service)
 
+        # Delete current ldap config
+        try:
+            self.send_request('ldap-config-delete')
+        except netapp_api.NaApiError as e:
+            if e.code != netapp_api.EOBJECTNOTFOUND:
+                msg = _("An error occurred while deleting original LDAP "
+                        "configuration. %s")
+                raise exception.NetAppException(msg % e.message)
+            else:
+                msg = _("Original LDAP configuration was not found. "
+                        "LDAP modification will continue.")
+                LOG.debug(msg)
+
+        new_config_name = (
+            hashlib.md5(six.b(new_security_service['id'])).hexdigest())
+        # Create ldap config with the new client
+        api_args = {'client-config': new_config_name, 'client-enabled': 'true'}
+        self.send_request('ldap-config-create', api_args)
+
+        # Delete old client configuration
+        try:
+            self._delete_ldap_client(current_security_service)
+        except netapp_api.NaApiError as e:
+            if e.code != netapp_api.EOBJECTNOTFOUND:
+                msg = _("An error occurred while deleting original LDAP "
+                        "client configuration. %s")
+                raise exception.NetAppException(msg % e.message)
+            else:
+                msg = _("Original LDAP client configuration was not found.")
+                LOG.debug(msg)
+
+    @na_utils.trace
+    def _get_cifs_server_name(self, vserver_name):
         # 'cifs-server' is CIFS Server NetBIOS Name, max length is 15.
         # Should be unique within each domain (data['domain']).
         # Cut to 15 char with begin and end, attempt to make valid DNS hostname
         cifs_server = (vserver_name[0:8] +
                        '-' +
                        vserver_name[-6:]).replace('_', '-').upper()
+        return cifs_server
+
+    @na_utils.trace
+    def configure_active_directory(self, security_service, vserver_name):
+        """Configures AD on Vserver."""
+        self.configure_dns(security_service)
+        self.set_preferred_dc(security_service)
+
+        cifs_server = self._get_cifs_server_name(vserver_name)
 
         api_args = {
             'admin-username': security_service['user'],
@@ -1628,6 +1676,46 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
             msg = _("Failed to create CIFS server entry. %s")
             raise exception.NetAppException(msg % e.message)
 
+    @na_utils.trace
+    def modify_active_directory_security_service(
+            self, vserver_name, differring_keys, new_security_service,
+            current_security_service):
+        cifs_server = self._get_cifs_server_name(vserver_name)
+
+        current_user_name = current_security_service['user']
+        new_username = new_security_service['user']
+
+        current_cifs_username = cifs_server + '\\' + current_user_name
+
+        if 'password' in differring_keys:
+            api_args = {
+                'user-name': current_cifs_username,
+                'user-password': new_security_service['password']
+            }
+            try:
+                self.send_request('cifs-local-user-set-password', api_args)
+            except netapp_api.NaApiError as e:
+                msg = _("Failed to modify existing CIFS server password. %s")
+                raise exception.NetAppException(msg % e.message)
+
+        if 'user' in differring_keys:
+            api_args = {
+                'user-name': current_cifs_username,
+                'new-user-name': new_username
+            }
+            try:
+                self.send_request('cifs-local-user-rename', api_args)
+            except netapp_api.NaApiError as e:
+                msg = _("Failed to modify existing CIFS server user-name. %s")
+                raise exception.NetAppException(msg % e.message)
+
+        if 'server' in differring_keys:
+            if current_security_service['server'] is not None:
+                self.remove_preferred_dcs(current_security_service)
+
+            if new_security_service['server'] is not None:
+                self.set_preferred_dc(new_security_service)
+
     @na_utils.trace
     def create_kerberos_realm(self, security_service):
         """Creates Kerberos realm on cluster."""
@@ -1694,6 +1782,26 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
                 security_service['domain'] + '@' +
                 security_service['domain'].upper())
 
+    @na_utils.trace
+    def update_kerberos_realm(self, security_service):
+        """Update Kerberos realm info. Only KDC IP can be changed."""
+        if not self.features.KERBEROS_VSERVER:
+            msg = _('Kerberos realms owned by Vserver are supported on ONTAP '
+                    '8.3 or later.')
+            raise exception.NetAppException(msg)
+
+        api_args = {
+            'admin-server-ip': security_service['server'],
+            'kdc-ip': security_service['server'],
+            'password-server-ip': security_service['server'],
+            'realm': security_service['domain'].upper(),
+        }
+        try:
+            self.send_request('kerberos-realm-modify', api_args)
+        except netapp_api.NaApiError as e:
+            msg = _('Failed to update Kerberos realm. %s')
+            raise exception.NetAppException(msg % e.message)
+
     @na_utils.trace
     def disable_kerberos(self, security_service):
         """Disable Kerberos in all Vserver LIFs."""
@@ -1819,6 +1927,36 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
                                  for server in servers.get_children()]
         return dns_config
 
+    @na_utils.trace
+    def update_dns_configuration(self, dns_ips, domains):
+        """Overrides DNS configuration with the specified IPs and domains."""
+        current_dns_config = self.get_dns_config()
+        api_args = {
+            'domains': [],
+            'name-servers': [],
+            'dns-state': 'enabled',
+        }
+        for domain in domains:
+            api_args['domains'].append({'string': domain})
+
+        for dns_ip in dns_ips:
+            api_args['name-servers'].append({'ip-address': dns_ip})
+
+        empty_dns_config = (not api_args['domains'] and
+                            not api_args['name-servers'])
+        if current_dns_config:
+            api_name, api_args = (
+                ('net-dns-destroy', {}) if empty_dns_config
+                else ('net-dns-modify', api_args))
+        else:
+            api_name, api_args = 'net-dns-create', api_args
+
+        try:
+            self.send_request(api_name, api_args)
+        except netapp_api.NaApiError as e:
+            msg = _("Failed to update DNS configuration. %s")
+            raise exception.NetAppException(msg % e.message)
+
     @na_utils.trace
     def set_preferred_dc(self, security_service):
         # server is optional
@@ -1842,6 +1980,20 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
             msg = _("Failed to set preferred DC. %s")
             raise exception.NetAppException(msg % e.message)
 
+    @na_utils.trace
+    def remove_preferred_dcs(self, security_service):
+        """Drops all preferred DCs at once."""
+
+        api_args = {
+            'domain': security_service['domain'],
+        }
+
+        try:
+            self.send_request('cifs-domain-preferred-dc-remove', api_args)
+        except netapp_api.NaApiError as e:
+            msg = _("Failed to unset preferred DCs. %s")
+            raise exception.NetAppException(msg % e.message)
+
     @na_utils.trace
     def create_volume(self, aggregate_name, volume_name, size_gb,
                       thin_provisioned=False, snapshot_policy=None,
diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py b/manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py
index f7a9286c0e..d0f495b197 100644
--- a/manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py
+++ b/manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py
@@ -35,6 +35,9 @@ class NetAppCmodeMultiSvmShareDriver(driver.ShareDriver):
             True, *args, **kwargs)
         self.library = lib_multi_svm.NetAppCmodeMultiSVMFileStorageLibrary(
             self.DRIVER_NAME, **kwargs)
+        # NetApp driver supports updating security service for in use share
+        # networks.
+        self.security_service_update_support = True
 
     def do_setup(self, context):
         self.library.do_setup(context)
@@ -336,3 +339,19 @@ class NetAppCmodeMultiSvmShareDriver(driver.ShareDriver):
                                             snapshots):
         return self.library.share_server_migration_get_progress(
             context, src_share_server, dest_share_server, shares, snapshots)
+
+    def update_share_server_security_service(
+            self, context, share_server, network_info, share_instances,
+            share_instance_rules, new_security_service,
+            current_security_service=None):
+        return self.library.update_share_server_security_service(
+            context, share_server, network_info, new_security_service,
+            current_security_service=current_security_service)
+
+    def check_update_share_server_security_service(
+            self, context, share_server, network_info, share_instances,
+            share_instance_rules, new_security_service,
+            current_security_service=None):
+        return self.library.check_update_share_server_security_service(
+            context, share_server, network_info, new_security_service,
+            current_security_service=current_security_service)
diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/drv_single_svm.py b/manila/share/drivers/netapp/dataontap/cluster_mode/drv_single_svm.py
index fd12c13e91..5853fe3158 100644
--- a/manila/share/drivers/netapp/dataontap/cluster_mode/drv_single_svm.py
+++ b/manila/share/drivers/netapp/dataontap/cluster_mode/drv_single_svm.py
@@ -319,3 +319,15 @@ class NetAppCmodeSingleSvmShareDriver(driver.ShareDriver):
             self, context, share_servers, share_group_ref,
             share_group_snapshot=None):
         raise NotImplementedError
+
+    def update_share_server_security_service(
+            self, context, share_server, network_info, share_instances,
+            share_instance_rules, new_security_service,
+            current_security_service=None):
+        raise NotImplementedError
+
+    def check_update_share_server_security_service(
+            self, context, share_server, network_info, share_instances,
+            share_instance_rules, new_security_service,
+            current_security_service=None):
+        raise NotImplementedError
diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py
index 3e674ddee1..c6ce5f3020 100644
--- a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py
+++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py
@@ -37,6 +37,7 @@ from manila.common import constants
 from manila import coordination
 from manila import exception
 from manila.i18n import _
+from manila.message import api as message_api
 from manila.share.drivers.netapp.dataontap.client import api as netapp_api
 from manila.share.drivers.netapp.dataontap.client import client_cmode
 from manila.share.drivers.netapp.dataontap.cluster_mode import data_motion
@@ -161,6 +162,7 @@ class NetAppCmodeFileStorageLibrary(object):
                                self.configuration.netapp_api_trace_pattern)
         self._backend_name = self.configuration.safe_get(
             'share_backend_name') or driver_name
+        self.message_api = message_api.API()
 
     @na_utils.trace
     def do_setup(self, context):
@@ -440,6 +442,7 @@ class NetAppCmodeFileStorageLibrary(object):
                 'snapshot_support': True,
                 'create_share_from_snapshot_support': True,
                 'revert_to_snapshot_support': self._revert_to_snapshot_support,
+                'security_service_update_support': True,
             }
 
             # Add storage service catalog data.
diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_multi_svm.py b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_multi_svm.py
index b18547243a..410e64b91c 100644
--- a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_multi_svm.py
+++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_multi_svm.py
@@ -29,6 +29,7 @@ from oslo_utils import units
 
 from manila import exception
 from manila.i18n import _
+from manila.message import message_field
 from manila.share.drivers.netapp.dataontap.client import api as netapp_api
 from manila.share.drivers.netapp.dataontap.client import client_cmode
 from manila.share.drivers.netapp.dataontap.cluster_mode import data_motion
@@ -1365,3 +1366,157 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
             .validate_provisioning_options_for_share(provisioning_options,
                                                      extra_specs=extra_specs,
                                                      qos_specs=qos_specs))
+
+    def _get_different_keys_for_equal_ss_type(self, current_sec_service,
+                                              new_sec_service):
+        different_keys = []
+
+        valid_keys = ['dns_ip', 'server', 'domain', 'user', 'password', 'ou']
+        for key, value in current_sec_service.items():
+            if (current_sec_service[key] != new_sec_service[key]
+                    and key in valid_keys):
+                different_keys.append(key)
+
+        return different_keys
+
+    def _is_security_service_valid(self, security_service):
+        mandatory_params = {
+            'ldap': ['user', 'password'],
+            'active_directory': ['dns_ip', 'domain', 'user', 'password'],
+            'kerberos': ['dns_ip', 'domain', 'user', 'password', 'server'],
+        }
+        ss_type = security_service['type']
+        if ss_type == 'ldap':
+            ad_domain = security_service.get('domain')
+            ldap_servers = security_service.get('server')
+            if not bool(ad_domain) ^ bool(ldap_servers):
+                msg = _("LDAP security service must have either 'server' or "
+                        "'domain' parameters. Use 'server' for Linux/Unix "
+                        "LDAP servers or 'domain' for Active Directory LDAP "
+                        "server.")
+                LOG.error(msg)
+                return False
+
+        if not all([security_service[key] is not None
+                    for key in mandatory_params[ss_type]]):
+            msg = _("The security service %s does not have all the "
+                    "parameters needed to used by the share driver."
+                    ) % security_service['id']
+            LOG.error(msg)
+            return False
+
+        return True
+
+    def update_share_server_security_service(self, context, share_server,
+                                             network_info,
+                                             new_security_service,
+                                             current_security_service=None):
+        current_type = (
+            current_security_service['type'].lower()
+            if current_security_service else '')
+        new_type = new_security_service['type'].lower()
+
+        vserver_name, vserver_client = self._get_vserver(
+            share_server=share_server)
+
+        # Check if this update is supported by our driver
+        if not self.check_update_share_server_security_service(
+                context, share_server, network_info, new_security_service,
+                current_security_service=current_security_service):
+            msg = _("The requested security service update is not supported "
+                    "by the NetApp driver.")
+            LOG.exception(msg)
+            raise exception.NetAppException(msg)
+
+        if current_security_service is None:
+            self._client.setup_security_services([new_security_service],
+                                                 vserver_client,
+                                                 vserver_name)
+            LOG.info("A new security service configuration was added to share "
+                     "server '%(share_server_id)s'",
+                     {'share_server_id': share_server['id']})
+            return
+
+        different_keys = self._get_different_keys_for_equal_ss_type(
+            current_security_service, new_security_service)
+        if not different_keys:
+            msg = _("The former and the latter security services are "
+                    "equal. Nothing to do.")
+            LOG.debug(msg)
+            return
+
+        if 'dns_ip' in different_keys:
+            dns_ips = set()
+            domains = set()
+            # Read all dns-ips and domains from other security services
+            for sec_svc in network_info['security_services']:
+                if sec_svc['type'] == current_type:
+                    # skip the one that we are replacing
+                    continue
+                if sec_svc.get('dns_ip') is not None:
+                    for dns_ip in sec_svc['dns_ip'].split(','):
+                        dns_ips.add(dns_ip.strip())
+                if sec_svc.get('domain') is not None:
+                    domains.add(sec_svc['domain'])
+            # Merge with the new dns configuration
+            if new_security_service.get('dns_ip') is not None:
+                for dns_ip in new_security_service['dns_ip'].split(','):
+                    dns_ips.add(dns_ip.strip())
+            if new_security_service.get('domain') is not None:
+                domains.add(new_security_service['domain'])
+
+            # Update vserver DNS configuration
+            vserver_client.update_dns_configuration(dns_ips, domains)
+
+        if new_type == 'kerberos':
+            if 'server' in different_keys:
+                # NOTE(dviroel): Only IPs will be updated here, new principals
+                # won't be configured here. It is expected that only the IP was
+                # changed, but the KDC remains the same.
+                LOG.debug('Updating kerberos realm on NetApp backend.')
+                vserver_client.update_kerberos_realm(new_security_service)
+
+        elif new_type == 'active_directory':
+            vserver_client.modify_active_directory_security_service(
+                vserver_name, different_keys, new_security_service,
+                current_security_service)
+        else:
+            vserver_client.modify_ldap(new_security_service,
+                                       current_security_service)
+
+        LOG.info("Security service configuration was updated for share server "
+                 "'%(share_server_id)s'",
+                 {'share_server_id': share_server['id']})
+
+    def check_update_share_server_security_service(
+            self, context, share_server, network_info,
+            new_security_service, current_security_service=None):
+        current_type = (
+            current_security_service['type'].lower()
+            if current_security_service else '')
+
+        if not self._is_security_service_valid(new_security_service):
+            self.message_api.create(
+                context,
+                message_field.Action.ADD_UPDATE_SECURITY_SERVICE,
+                new_security_service['project_id'],
+                resource_type=message_field.Resource.SECURITY_SERVICE,
+                resource_id=new_security_service['id'],
+                detail=(message_field.Detail
+                        .UNSUPPORTED_ADD_UDPATE_SECURITY_SERVICE))
+            return False
+
+        if current_security_service:
+            if current_type != 'ldap':
+                # NOTE(dviroel): We don't support domain/realm updates for
+                # Kerberos security service, because it might require a new SPN
+                # to be created and to destroy the old one, thus disrupting all
+                # shares hosted by this share server. Same issue can happen
+                # with AD domain modifications.
+                if (current_security_service['domain'].lower() !=
+                        new_security_service['domain'].lower()):
+                    msg = _("Currently the driver does not support updates "
+                            "in the security service 'domain'.")
+                    LOG.info(msg)
+                    return False
+        return True
diff --git a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py
index 13f6677a14..2bd06e4626 100644
--- a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py
+++ b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py
@@ -471,7 +471,17 @@ CIFS_SECURITY_SERVICE = {
     'ou': 'fake_ou',
     'domain': 'fake_domain',
     'dns_ip': 'fake_dns_ip',
-    'server': '',
+    'server': 'fake_server',
+}
+
+CIFS_SECURITY_SERVICE_2 = {
+    'type': 'active_directory',
+    'password': 'fake_password_2',
+    'user': 'fake_user_2',
+    'ou': 'fake_ou_2',
+    'domain': 'fake_domain_2',
+    'dns_ip': 'fake_dns_ip_2',
+    'server': 'fake_server_2',
 }
 
 LDAP_LINUX_SECURITY_SERVICE = {
@@ -496,6 +506,7 @@ LDAP_AD_SECURITY_SERVICE = {
     'server': None,
 }
 
+
 KERBEROS_SECURITY_SERVICE = {
     'type': 'kerberos',
     'password': 'fake_password',
diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py
index e910361136..4018eb8563 100644
--- a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py
+++ b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py
@@ -2670,6 +2670,42 @@ class NetAppClientCmodeTestCase(test.TestCase):
                           self.client.create_kerberos_realm,
                           fake.KERBEROS_SECURITY_SERVICE)
 
+    def test_update_kerberos_realm(self):
+        self.client.features.add_feature('KERBEROS_VSERVER')
+        self.mock_object(self.client, 'send_request')
+
+        self.client.update_kerberos_realm(fake.KERBEROS_SECURITY_SERVICE)
+
+        kerberos_realm_create_args = {
+            'admin-server-ip': fake.KERBEROS_SECURITY_SERVICE['server'],
+            'kdc-ip': fake.KERBEROS_SECURITY_SERVICE['server'],
+            'password-server-ip': fake.KERBEROS_SECURITY_SERVICE['server'],
+            'realm': fake.KERBEROS_SECURITY_SERVICE['domain'].upper(),
+        }
+
+        self.client.send_request.assert_has_calls([
+            mock.call('kerberos-realm-modify',
+                      kerberos_realm_create_args)])
+
+    def test_update_kerberos_realm_failure(self):
+        self.client.features.add_feature('KERBEROS_VSERVER')
+        self.mock_object(self.client, 'send_request', self._mock_api_error())
+
+        self.assertRaises(exception.NetAppException,
+                          self.client.update_kerberos_realm,
+                          fake.KERBEROS_SECURITY_SERVICE)
+
+        kerberos_realm_create_args = {
+            'admin-server-ip': fake.KERBEROS_SECURITY_SERVICE['server'],
+            'kdc-ip': fake.KERBEROS_SECURITY_SERVICE['server'],
+            'password-server-ip': fake.KERBEROS_SECURITY_SERVICE['server'],
+            'realm': fake.KERBEROS_SECURITY_SERVICE['domain'].upper(),
+        }
+
+        self.client.send_request.assert_has_calls([
+            mock.call('kerberos-realm-modify',
+                      kerberos_realm_create_args)])
+
     def test_configure_kerberos(self):
         self.client.features.add_feature('KERBEROS_VSERVER')
         self.mock_object(self.client, 'send_request')
@@ -2880,6 +2916,34 @@ class NetAppClientCmodeTestCase(test.TestCase):
         self.client.send_request.assert_has_calls([
             mock.call('net-dns-modify', net_dns_create_args)])
 
+    def test_update_dns_configuration(self):
+        fake_configured_dns = {
+            'dns-state': 'enabled',
+            'domains': ['fake_domain_2'],
+            'dns-ips': ['fake_dns_ip_2']
+        }
+        self.mock_object(self.client, 'get_dns_config',
+                         mock.Mock(return_value=fake_configured_dns))
+        self.mock_object(self.client, 'send_request')
+
+        self.client.configure_dns(fake.KERBEROS_SECURITY_SERVICE)
+        domains = set()
+        domains.add(fake_configured_dns['domains'][0])
+        domains.add(fake.KERBEROS_SECURITY_SERVICE['domain'])
+
+        dns_ips = set()
+        dns_ips.add(fake_configured_dns['dns-ips'][0])
+        dns_ips.add(fake.KERBEROS_SECURITY_SERVICE['dns_ip'])
+
+        net_dns_create_args = {
+            'domains': [{'string': domain} for domain in domains],
+            'dns-state': 'enabled',
+            'name-servers': [{'ip-address': dns_ip} for dns_ip in dns_ips]
+        }
+
+        self.client.send_request.assert_has_calls([
+            mock.call('net-dns-modify', net_dns_create_args)])
+
     def test_configure_dns_api_error(self):
 
         self.mock_object(self.client, 'send_request', self._mock_api_error())
@@ -2960,6 +3024,34 @@ class NetAppClientCmodeTestCase(test.TestCase):
                           self.client.set_preferred_dc,
                           security_service)
 
+    def test_remove_preferred_dcs(self):
+        self.mock_object(self.client, 'send_request')
+        security_service = copy.deepcopy(fake.CIFS_SECURITY_SERVICE)
+
+        self.client.remove_preferred_dcs(security_service)
+
+        preferred_dc_add_args = {
+            'domain': security_service['domain'],
+        }
+        self.client.send_request.assert_has_calls([
+            mock.call('cifs-domain-preferred-dc-remove',
+                      preferred_dc_add_args)])
+
+    def test_remove_preferred_dcs_error(self):
+        self.mock_object(self.client, 'send_request', self._mock_api_error())
+        security_service = copy.deepcopy(fake.CIFS_SECURITY_SERVICE)
+
+        self.assertRaises(exception.NetAppException,
+                          self.client.remove_preferred_dcs,
+                          security_service)
+
+        preferred_dc_add_args = {
+            'domain': security_service['domain'],
+        }
+        self.client.send_request.assert_has_calls([
+            mock.call('cifs-domain-preferred-dc-remove',
+                      preferred_dc_add_args)])
+
     def test_create_volume(self):
 
         self.mock_object(self.client, 'send_request')
@@ -7622,6 +7714,114 @@ class NetAppClientCmodeTestCase(test.TestCase):
         self.client.send_request.assert_called_once_with(
             'volume-autosize-get', {'volume': fake.SHARE_NAME})
 
+    def test_modify_active_directory_security_service(self):
+        curr_sec_service = copy.deepcopy(fake.CIFS_SECURITY_SERVICE)
+        new_sec_service = copy.deepcopy(fake.CIFS_SECURITY_SERVICE_2)
+        # we don't support domain change, but this validation isn't made in
+        # within this method
+        new_sec_service['domain'] = curr_sec_service['domain']
+        api_responses = [fake.PASSED_RESPONSE, fake.PASSED_RESPONSE]
+
+        self.mock_object(self.client, 'send_request',
+                         mock.Mock(side_effect=api_responses))
+        self.mock_object(self.client, 'remove_preferred_dcs')
+        self.mock_object(self.client, 'set_preferred_dc')
+        differing_keys = {'password', 'user', 'server'}
+
+        self.client.modify_active_directory_security_service(
+            fake.VSERVER_NAME, differing_keys, new_sec_service,
+            curr_sec_service)
+
+        cifs_server = self.client._get_cifs_server_name(fake.VSERVER_NAME)
+        current_cifs_username = cifs_server + '\\' + curr_sec_service['user']
+        set_pass_api_args = {
+            'user-name': current_cifs_username,
+            'user-password': new_sec_service['password']
+        }
+        user_rename_api_args = {
+            'user-name': current_cifs_username,
+            'new-user-name': new_sec_service['user']
+        }
+
+        self.client.send_request.assert_has_calls([
+            mock.call('cifs-local-user-set-password', set_pass_api_args),
+            mock.call('cifs-local-user-rename', user_rename_api_args)])
+        self.client.remove_preferred_dcs.assert_called_once_with(
+            curr_sec_service)
+        self.client.set_preferred_dc.assert_called_once_with(new_sec_service)
+
+    @ddt.data(True, False)
+    def test_modify_active_directory_security_service_error(
+            self, cifs_set_password_failure):
+        curr_sec_service = copy.deepcopy(fake.CIFS_SECURITY_SERVICE)
+        new_sec_service = copy.deepcopy(fake.CIFS_SECURITY_SERVICE_2)
+        # we don't support domain change, but this validation isn't made in
+        # within this method
+        new_sec_service['domain'] = curr_sec_service['domain']
+        if cifs_set_password_failure:
+            api_responses = [netapp_api.NaApiError(code='fake'),
+                             fake.PASSED_RESPONSE]
+        else:
+            api_responses = [fake.PASSED_RESPONSE,
+                             netapp_api.NaApiError(code='fake')]
+
+        self.mock_object(self.client, 'send_request',
+                         mock.Mock(side_effect=api_responses))
+        differing_keys = {'password', 'user', 'server'}
+
+        self.assertRaises(
+            exception.NetAppException,
+            self.client.modify_active_directory_security_service,
+            fake.VSERVER_NAME, differing_keys, new_sec_service,
+            curr_sec_service)
+
+        cifs_server = self.client._get_cifs_server_name(fake.VSERVER_NAME)
+        current_cifs_username = cifs_server + '\\' + curr_sec_service['user']
+        set_pass_api_args = {
+            'user-name': current_cifs_username,
+            'user-password': new_sec_service['password']
+        }
+        user_rename_api_args = {
+            'user-name': current_cifs_username,
+            'new-user-name': new_sec_service['user']
+        }
+
+        if cifs_set_password_failure:
+            send_request_calls = [
+                mock.call('cifs-local-user-set-password', set_pass_api_args)]
+        else:
+            send_request_calls = [
+                mock.call('cifs-local-user-set-password', set_pass_api_args),
+                mock.call('cifs-local-user-rename', user_rename_api_args)
+            ]
+
+        self.client.send_request.assert_has_calls(send_request_calls)
+
+    @ddt.data(False, True)
+    def test_modify_ldap(self, api_not_found):
+        current_ldap_service = fake.LDAP_AD_SECURITY_SERVICE
+        new_ldap_service = fake.LDAP_LINUX_SECURITY_SERVICE
+        config_name = hashlib.md5(six.b(new_ldap_service['id'])).hexdigest()
+        api_result = (self._mock_api_error(code=netapp_api.EOBJECTNOTFOUND)
+                      if api_not_found else mock.Mock())
+        mock_create_client = self.mock_object(
+            self.client, '_create_ldap_client')
+        mock_send_request = self.mock_object(
+            self.client, 'send_request',
+            mock.Mock(return_value=api_result))
+        mock_delete_client = self.mock_object(
+            self.client, '_delete_ldap_client',
+            mock.Mock(return_value=api_result))
+
+        self.client.modify_ldap(new_ldap_service, current_ldap_service)
+
+        api_args = {'client-config': config_name, 'client-enabled': 'true'}
+        mock_create_client.assert_called_once_with(new_ldap_service)
+        mock_send_request.assert_has_calls([
+            mock.call('ldap-config-delete'),
+            mock.call('ldap-config-create', api_args)])
+        mock_delete_client.assert_called_once_with(current_ldap_service)
+
     def test_create_fpolicy_event(self):
         self.mock_object(self.client, 'send_request')
 
diff --git a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py
index e481056df1..29abc0d5ca 100644
--- a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py
+++ b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py
@@ -2627,3 +2627,164 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
             exception.NetAppException,
             self.library.validate_provisioning_options_for_share,
             fake.PROVISIONING_OPTS_WITH_ADAPT_QOS, qos_specs=None)
+
+    def test__get_different_keys_for_equal_ss_type(self):
+        curr_sec_service = copy.deepcopy(fake.CIFS_SECURITY_SERVICE)
+        new_sec_service = copy.deepcopy(fake.CIFS_SECURITY_SERVICE_2)
+
+        expected_keys = ['password', 'user', 'ou',
+                         'domain', 'dns_ip', 'server']
+
+        result = self.library._get_different_keys_for_equal_ss_type(
+            curr_sec_service, new_sec_service)
+
+        self.assertEqual(expected_keys, result)
+
+    @ddt.data(
+        {'current': None,
+         'new': fake.CIFS_SECURITY_SERVICE,
+         'existing': []},
+
+        {'current': fake.CIFS_SECURITY_SERVICE,
+         'new': fake.CIFS_SECURITY_SERVICE_2,
+         'existing': [fake.CIFS_SECURITY_SERVICE,
+                      fake.KERBEROS_SECURITY_SERVICE]},
+
+        {'current': fake.KERBEROS_SECURITY_SERVICE,
+         'new': fake.KERBEROS_SECURITY_SERVICE_2,
+         'existing': [fake.CIFS_SECURITY_SERVICE,
+                      fake.KERBEROS_SECURITY_SERVICE]},
+
+        {'current': fake.CIFS_SECURITY_SERVICE,
+         'new': fake.CIFS_SECURITY_SERVICE,
+         'existing': [fake.CIFS_SECURITY_SERVICE]},
+    )
+    @ddt.unpack
+    def test_update_share_server_security_service(self, current, new,
+                                                  existing):
+        fake_context = mock.Mock()
+        fake_net_info = copy.deepcopy(fake.NETWORK_INFO)
+        new_sec_service = copy.deepcopy(new)
+        curr_sec_service = copy.deepcopy(current) if current else None
+        new_type = new_sec_service['type'].lower()
+        fake_net_info['security_services'] = existing
+
+        if curr_sec_service:
+            # domain modification aren't support
+            new_sec_service['domain'] = curr_sec_service['domain']
+
+        different_keys = []
+        if curr_sec_service != new_sec_service:
+            different_keys = ['dns_ip', 'server', 'domain', 'user', 'password']
+            if new_sec_service.get('ou') is not None:
+                different_keys.append('ou')
+
+        fake_vserver_client = mock.Mock()
+        mock_get_vserver = self.mock_object(
+            self.library, '_get_vserver',
+            mock.Mock(return_value=[fake.VSERVER1, fake_vserver_client]))
+        mock_check_update = self.mock_object(
+            self.library, 'check_update_share_server_security_service',
+            mock.Mock(return_value=True))
+        mock_setup_sec_serv = self.mock_object(
+            self.library._client, 'setup_security_services')
+        mock_diff_keys = self.mock_object(
+            self.library, '_get_different_keys_for_equal_ss_type',
+            mock.Mock(return_value=different_keys))
+        mock_dns_update = self.mock_object(
+            fake_vserver_client, 'update_dns_configuration')
+        mock_update_krealm = self.mock_object(
+            fake_vserver_client, 'update_kerberos_realm')
+        mock_modify_ad = self.mock_object(
+            fake_vserver_client, 'modify_active_directory_security_service')
+
+        self.library.update_share_server_security_service(
+            fake_context, fake.SHARE_SERVER, fake_net_info,
+            new_sec_service, current_security_service=curr_sec_service)
+
+        dns_ips = set()
+        domains = set()
+        # we don't need to split and strip since we know that fake have only
+        # on dns-ip and domain configured
+        for ss in existing:
+            if ss['type'] != new_sec_service['type']:
+                dns_ips.add(ss['dns_ip'])
+                domains.add(ss['domain'])
+        dns_ips.add(new_sec_service['dns_ip'])
+        domains.add(new_sec_service['domain'])
+
+        mock_get_vserver.assert_called_once_with(
+            share_server=fake.SHARE_SERVER)
+        mock_check_update.assert_called_once_with(
+            fake_context, fake.SHARE_SERVER, fake_net_info, new_sec_service,
+            current_security_service=curr_sec_service)
+
+        if curr_sec_service is None:
+            mock_setup_sec_serv.assert_called_once_with(
+                [new_sec_service], fake_vserver_client, fake.VSERVER1)
+        else:
+            mock_diff_keys.assert_called_once_with(curr_sec_service,
+                                                   new_sec_service)
+            if different_keys:
+                mock_dns_update.assert_called_once_with(dns_ips, domains)
+                if new_type == 'kerberos':
+                    mock_update_krealm.assert_called_once_with(new_sec_service)
+                elif new_type == 'active_directory':
+                    mock_modify_ad.assert_called_once_with(
+                        fake.VSERVER1, different_keys, new_sec_service,
+                        curr_sec_service)
+
+    def test_update_share_server_security_service_check_error(self):
+        curr_sec_service = copy.deepcopy(fake.CIFS_SECURITY_SERVICE)
+        new_sec_service = copy.deepcopy(fake.CIFS_SECURITY_SERVICE_2)
+        fake_vserver_client = mock.Mock()
+        fake_context = mock.Mock()
+        fake_net_info = mock.Mock()
+
+        mock_get_vserver = self.mock_object(
+            self.library, '_get_vserver',
+            mock.Mock(return_value=[fake.VSERVER1, fake_vserver_client]))
+        mock_check_update = self.mock_object(
+            self.library, 'check_update_share_server_security_service',
+            mock.Mock(return_value=False))
+
+        self.assertRaises(
+            exception.NetAppException,
+            self.library.update_share_server_security_service,
+            fake_context, fake.SHARE_SERVER, fake_net_info,
+            new_sec_service, current_security_service=curr_sec_service)
+
+        mock_get_vserver.assert_called_once_with(
+            share_server=fake.SHARE_SERVER)
+        mock_check_update.assert_called_once_with(
+            fake_context, fake.SHARE_SERVER, fake_net_info,
+            new_sec_service, current_security_service=curr_sec_service)
+
+    @ddt.data(
+        {'new': fake.LDAP_AD_SECURITY_SERVICE,
+         'current': fake.LDAP_LINUX_SECURITY_SERVICE,
+         'expected': True},
+
+        {'new': fake.CIFS_SECURITY_SERVICE,
+         'current': fake.KERBEROS_SECURITY_SERVICE,
+         'expected': False},
+
+        {'new': fake.CIFS_SECURITY_SERVICE,
+         'current': fake.CIFS_SECURITY_SERVICE,
+         'expected': True},
+
+        {'new': fake.KERBEROS_SECURITY_SERVICE,
+         'current': fake.KERBEROS_SECURITY_SERVICE,
+         'expected': True},
+
+        {'new': fake.CIFS_SECURITY_SERVICE,
+         'current': None,
+         'expected': True},
+    )
+    @ddt.unpack
+    def test_check_update_share_server_security_service(self, new, current,
+                                                        expected):
+        result = self.library.check_update_share_server_security_service(
+            None, None, None, new, current_security_service=current)
+
+        self.assertEqual(expected, result)
diff --git a/manila/tests/share/drivers/netapp/dataontap/fakes.py b/manila/tests/share/drivers/netapp/dataontap/fakes.py
index 03481b5e53..63381a510f 100644
--- a/manila/tests/share/drivers/netapp/dataontap/fakes.py
+++ b/manila/tests/share/drivers/netapp/dataontap/fakes.py
@@ -911,6 +911,7 @@ POOLS = [
         'create_share_from_snapshot_support': True,
         'revert_to_snapshot_support': True,
         'qos': True,
+        'security_service_update_support': True,
     },
     {
         'pool_name': AGGREGATES[1],
@@ -934,6 +935,7 @@ POOLS = [
         'create_share_from_snapshot_support': True,
         'revert_to_snapshot_support': True,
         'qos': True,
+        'security_service_update_support': True,
     },
 ]
 
@@ -957,6 +959,7 @@ POOLS_VSERVER_CREDS = [
         'create_share_from_snapshot_support': True,
         'revert_to_snapshot_support': True,
         'qos': False,
+        'security_service_update_support': True,
     },
     {
         'pool_name': AGGREGATES[1],
@@ -977,6 +980,7 @@ POOLS_VSERVER_CREDS = [
         'create_share_from_snapshot_support': True,
         'revert_to_snapshot_support': True,
         'qos': False,
+        'security_service_update_support': True,
     },
 ]
 
@@ -1555,7 +1559,60 @@ CIFS_SECURITY_SERVICE = {
     'ou': 'fake_ou',
     'domain': 'fake_domain',
     'dns_ip': 'fake_dns_ip',
-    'server': '',
+    'server': 'fake_server',
+}
+
+CIFS_SECURITY_SERVICE_2 = {
+    'id': 'fake_id_2',
+    'type': 'active_directory',
+    'password': 'fake_password_2',
+    'user': 'fake_user_2',
+    'ou': 'fake_ou_2',
+    'domain': 'fake_domain_2',
+    'dns_ip': 'fake_dns_ip_2',
+    'server': 'fake_server_2',
+}
+LDAP_LINUX_SECURITY_SERVICE = {
+    'id': 'fake_id',
+    'type': 'ldap',
+    'user': 'fake_user',
+    'password': 'fake_password',
+    'server': 'fake_server',
+    'ou': 'fake_ou',
+    'dns_ip': None,
+    'domain': None
+}
+
+
+LDAP_AD_SECURITY_SERVICE = {
+    'id': 'fake_id',
+    'type': 'ldap',
+    'user': 'fake_user',
+    'password': 'fake_password',
+    'domain': 'fake_domain',
+    'ou': 'fake_ou',
+    'dns_ip': 'fake_dns_ip',
+    'server': None,
+}
+
+KERBEROS_SECURITY_SERVICE = {
+    'id': 'fake_id_3',
+    'type': 'kerberos',
+    'password': 'fake_password_3',
+    'user': 'fake_user_3',
+    'domain': 'fake_realm',
+    'dns_ip': 'fake_dns_ip_3',
+    'server': 'fake_server_3',
+}
+
+KERBEROS_SECURITY_SERVICE_2 = {
+    'id': 'fake_id_4',
+    'type': 'kerberos',
+    'password': 'fake_password_4',
+    'user': 'fake_user_4',
+    'domain': 'fake_realm_2',
+    'dns_ip': 'fake_dns_ip_4',
+    'server': 'fake_server_4',
 }
 
 SHARE_NETWORK_SUBNET = {
diff --git a/releasenotes/notes/netapp-add-security-service-update-718a68ebe60fd2b5.yaml b/releasenotes/notes/netapp-add-security-service-update-718a68ebe60fd2b5.yaml
new file mode 100644
index 0000000000..fdad623a8e
--- /dev/null
+++ b/releasenotes/notes/netapp-add-security-service-update-718a68ebe60fd2b5.yaml
@@ -0,0 +1,10 @@
+---
+features:
+  - |
+    NetApp ONTAP driver now supports add and update security services when
+    they are associated with in use share networks. Both add and update
+    operations are supported by all three security service types:
+    ``active_directory``, ``kerberos`` and ``ldap``. In order to update their
+    parameters in a non-disruptively way, ``active_directory`` and ``kerberos``
+    don't support ``domain`` updates.
+