Merge "Add security service update support to the container driver"
This commit is contained in:
commit
475eeafd8d
@ -64,6 +64,11 @@ container_opts = [
|
||||
default="manila.share.drivers.container.protocol_helper."
|
||||
"DockerCIFSHelper",
|
||||
help="Helper which facilitates interaction with share server."),
|
||||
cfg.StrOpt("container_security_service_helper",
|
||||
default="manila.share.drivers.container.security_service_helper"
|
||||
".SecurityServiceHelper",
|
||||
help="Helper which facilitates interaction with security "
|
||||
"services."),
|
||||
cfg.StrOpt("container_storage_helper",
|
||||
default="manila.share.drivers.container.storage_helper."
|
||||
"LVMHelper",
|
||||
@ -87,6 +92,9 @@ class ContainerShareDriver(driver.ShareDriver, driver.ExecuteMixin):
|
||||
self.container = importutils.import_class(
|
||||
self.configuration.container_helper)(
|
||||
configuration=self.configuration)
|
||||
self.security_service_helper = importutils.import_class(
|
||||
self.configuration.container_security_service_helper)(
|
||||
configuration=self.configuration)
|
||||
self.storage = importutils.import_class(
|
||||
self.configuration.container_storage_helper)(
|
||||
configuration=self.configuration)
|
||||
@ -118,7 +126,8 @@ class ContainerShareDriver(driver.ShareDriver, driver.ExecuteMixin):
|
||||
'snapshot_support': False,
|
||||
'create_share_from_snapshot_support': False,
|
||||
'driver_name': 'ContainerShareDriver',
|
||||
'pools': self.storage.get_share_server_pools()
|
||||
'pools': self.storage.get_share_server_pools(),
|
||||
'security_service_update_support': True
|
||||
}
|
||||
super(ContainerShareDriver, self)._update_share_stats(data)
|
||||
|
||||
@ -294,6 +303,11 @@ class ContainerShareDriver(driver.ShareDriver, driver.ExecuteMixin):
|
||||
except Exception as e:
|
||||
raise exception.ManilaException(_("Cannot create container: %s") %
|
||||
e)
|
||||
security_services = network_info.get('security_services')
|
||||
|
||||
if security_services:
|
||||
self.setup_security_services(server_id, security_services)
|
||||
|
||||
veths_after = self._get_veth_state()
|
||||
|
||||
veth = self._get_corresponding_veth(veths_before, veths_after)
|
||||
@ -549,3 +563,96 @@ class ContainerShareDriver(driver.ShareDriver, driver.ExecuteMixin):
|
||||
return {
|
||||
'share_updates': shares_updates,
|
||||
}
|
||||
|
||||
def setup_security_services(self, share_server_id, security_services):
|
||||
"""Is called to setup a security service in the share server."""
|
||||
|
||||
for security_service in security_services:
|
||||
if security_service['type'].lower() != 'ldap':
|
||||
raise exception.ShareBackendException(_(
|
||||
"The container driver does not support security services "
|
||||
"other than LDAP."))
|
||||
|
||||
self.security_service_helper.setup_security_service(
|
||||
share_server_id, security_service)
|
||||
|
||||
def _get_different_security_service_keys(
|
||||
self, current_security_service, new_security_service):
|
||||
valid_keys = ['dns_ip', 'server', 'domain', 'user', 'password', 'ou']
|
||||
different_keys = []
|
||||
for key, value in current_security_service.items():
|
||||
if (current_security_service[key] != new_security_service[key]
|
||||
and key in valid_keys):
|
||||
different_keys.append(key)
|
||||
return different_keys
|
||||
|
||||
def _check_if_all_fields_are_updatable(self, current_security_service,
|
||||
new_security_service):
|
||||
# NOTE(carloss): We only support updating user and password at
|
||||
# the moment
|
||||
updatable_fields = ['user', 'password']
|
||||
different_keys = self._get_different_security_service_keys(
|
||||
current_security_service, new_security_service)
|
||||
for key in different_keys:
|
||||
if key not in updatable_fields:
|
||||
return False
|
||||
return True
|
||||
|
||||
def update_share_server_security_service(self, context, share_server,
|
||||
network_info,
|
||||
share_instances,
|
||||
share_instance_rules,
|
||||
new_security_service,
|
||||
current_security_service=None):
|
||||
"""Is called to update or add a sec service to a share server."""
|
||||
|
||||
if not self.check_update_share_server_security_service(
|
||||
context, share_server, network_info, share_instances,
|
||||
share_instance_rules, new_security_service,
|
||||
current_security_service=current_security_service):
|
||||
raise exception.ManilaException(_(
|
||||
"The requested security service update is not supported by "
|
||||
"the container driver."))
|
||||
|
||||
server_id = self._get_container_name(share_server['id'])
|
||||
|
||||
if not current_security_service:
|
||||
self.setup_security_services(server_id, [new_security_service])
|
||||
else:
|
||||
self.security_service_helper.update_security_service(
|
||||
server_id, current_security_service, new_security_service)
|
||||
|
||||
msg = (
|
||||
"The security service was successfully added to the share "
|
||||
"server %(server_id)s.")
|
||||
msg_args = {
|
||||
'server_id': share_server['id'],
|
||||
}
|
||||
LOG.info(msg, msg_args)
|
||||
|
||||
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):
|
||||
current_type = (
|
||||
current_security_service['type'].lower()
|
||||
if current_security_service else '')
|
||||
new_type = new_security_service['type'].lower()
|
||||
|
||||
if new_type != 'ldap' or (current_type and current_type != 'ldap'):
|
||||
LOG.error('Currently only LDAP security services are supported '
|
||||
'by the container driver.')
|
||||
return False
|
||||
|
||||
if not current_type:
|
||||
return True
|
||||
|
||||
all_fields_are_updatable = self._check_if_all_fields_are_updatable(
|
||||
current_security_service, new_security_service)
|
||||
if not all_fields_are_updatable:
|
||||
LOG.info(
|
||||
"The Container driver does not support updating "
|
||||
"security service parameters other than 'user' and "
|
||||
"'password'.")
|
||||
return False
|
||||
return True
|
||||
|
107
manila/share/drivers/container/security_service_helper.py
Normal file
107
manila/share/drivers/container/security_service_helper.py
Normal file
@ -0,0 +1,107 @@
|
||||
# Copyright (c) 2021 NetApp, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
from manila.share import driver
|
||||
from manila import utils as manila_utils
|
||||
|
||||
# LDAP error codes
|
||||
LDAP_INVALID_CREDENTIALS = 49
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SecurityServiceHelper(driver.ExecuteMixin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.configuration = kwargs.pop("configuration", None)
|
||||
super(SecurityServiceHelper, self).__init__(*args, **kwargs)
|
||||
self.init_execute_mixin()
|
||||
|
||||
def setup_security_service(self, share_server_id, security_service):
|
||||
msg = ("Setting up the security service %(service)s for share server "
|
||||
"%(server_id)s")
|
||||
msg_args = {
|
||||
'service': security_service['id'],
|
||||
'server_id': share_server_id
|
||||
}
|
||||
LOG.debug(msg, msg_args)
|
||||
self.ldap_bind(share_server_id, security_service)
|
||||
|
||||
def update_security_service(self, server_id, current_security_service,
|
||||
new_security_service):
|
||||
msg = ("Updating the security service %(service)s for share server "
|
||||
"%(server_id)s")
|
||||
msg_args = {
|
||||
'service': new_security_service['id'],
|
||||
'server_id': server_id
|
||||
}
|
||||
LOG.debug(msg, msg_args)
|
||||
self.ldap_bind(server_id, new_security_service)
|
||||
|
||||
def ldap_bind(self, share_server_id, security_service):
|
||||
ss_info = self.ldap_get_info(security_service)
|
||||
cmd = ["docker", "exec", "%s" % share_server_id, "ldapwhoami", "-x",
|
||||
"-H", "ldap://localhost:389", "-D",
|
||||
"cn=%s,dc=example,dc=com" % ss_info["ss_user"], "-w", "%s" %
|
||||
ss_info["ss_password"]]
|
||||
self.ldap_retry_operation(cmd, run_as_root=True)
|
||||
|
||||
def ldap_get_info(self, security_service):
|
||||
if all(info in security_service for info in ("user", "password")):
|
||||
ss_user = security_service["user"]
|
||||
ss_password = security_service["password"]
|
||||
else:
|
||||
raise exception.ShareBackendException(
|
||||
_("LDAP requires user and password to be set for the bind "
|
||||
"operation."))
|
||||
ss_info = {
|
||||
"ss_user": ss_user,
|
||||
"ss_password": ss_password,
|
||||
}
|
||||
return ss_info
|
||||
|
||||
def ldap_retry_operation(self, cmd, run_as_root=True, timeout=30):
|
||||
interval = 5
|
||||
retries = int(timeout / interval) or 1
|
||||
|
||||
@manila_utils.retry(exception.ProcessExecutionError, interval=interval,
|
||||
retries=retries, backoff_rate=1)
|
||||
def try_ldap_operation():
|
||||
try:
|
||||
self._execute(*cmd, run_as_root=run_as_root)
|
||||
except exception.ProcessExecutionError as e:
|
||||
if e.exit_code == LDAP_INVALID_CREDENTIALS:
|
||||
msg = _('LDAP credentials are invalid. '
|
||||
'Aborting operation.')
|
||||
LOG.warning(msg)
|
||||
raise exception.ShareBackendException(msg=msg)
|
||||
else:
|
||||
msg = _('Command has returned execution error.'
|
||||
' Will retry the operation.'
|
||||
' Error details: %s') % e.stderr
|
||||
LOG.warning(msg)
|
||||
raise exception.ProcessExecutionError()
|
||||
|
||||
try:
|
||||
try_ldap_operation()
|
||||
except exception.ProcessExecutionError as e:
|
||||
msg = _("Unable to execute LDAP operation with success. "
|
||||
"Retries exhausted. Error details: %s") % e.stderr
|
||||
LOG.exception(msg)
|
||||
raise exception.ShareBackendException(msg=msg)
|
@ -27,6 +27,7 @@ from manila.share import configuration
|
||||
from manila.share.drivers.container import driver
|
||||
from manila.share.drivers.container import protocol_helper
|
||||
from manila import test
|
||||
from manila.tests import db_utils
|
||||
from manila.tests import fake_utils
|
||||
from manila.tests.share.drivers.container import fakes as cont_fakes
|
||||
|
||||
@ -608,3 +609,109 @@ class ContainerShareDriverTestCase(test.TestCase):
|
||||
|
||||
mock_get_container_name.assert_any_call(source_server['id'])
|
||||
mock_get_container_name.assert_any_call(dest_server['id'])
|
||||
|
||||
def test__get_different_security_service_keys(self):
|
||||
sec_service_keys = ['dns_ip', 'server', 'domain', 'user', 'password',
|
||||
'ou']
|
||||
current_security_service = {}
|
||||
[current_security_service.update({key: key + '_1'})
|
||||
for key in sec_service_keys]
|
||||
new_security_service = {}
|
||||
[new_security_service.update({key: key + '_2'})
|
||||
for key in sec_service_keys]
|
||||
|
||||
db_utils.create_security_service(**current_security_service)
|
||||
db_utils.create_security_service(**new_security_service)
|
||||
|
||||
different_keys = self._driver._get_different_security_service_keys(
|
||||
current_security_service, new_security_service)
|
||||
|
||||
[self.assertIn(key, different_keys) for key in sec_service_keys]
|
||||
|
||||
@ddt.data(
|
||||
(['dns_ip', 'server', 'domain', 'user', 'password', 'ou'], False),
|
||||
(['user', 'password'], True)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test__check_if_all_fields_are_updatable(self, keys, expected_result):
|
||||
|
||||
current_security_service = db_utils.create_security_service()
|
||||
new_security_service = db_utils.create_security_service()
|
||||
|
||||
mock_get_keys = self.mock_object(
|
||||
self._driver, '_get_different_security_service_keys',
|
||||
mock.Mock(return_value=keys))
|
||||
|
||||
result = self._driver._check_if_all_fields_are_updatable(
|
||||
current_security_service, new_security_service)
|
||||
|
||||
self.assertEqual(expected_result, result)
|
||||
mock_get_keys.assert_called_once_with(
|
||||
current_security_service, new_security_service
|
||||
)
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_update_share_server_security_service(
|
||||
self, with_current_service):
|
||||
new_security_service = db_utils.create_security_service()
|
||||
current_security_service = (
|
||||
db_utils.create_security_service()
|
||||
if with_current_service else None)
|
||||
share_server = db_utils.create_share_server()
|
||||
fake_container_name = 'fake_name'
|
||||
network_info = {}
|
||||
share_instances = []
|
||||
share_instance_access_rules = []
|
||||
|
||||
mock_check_update = self.mock_object(
|
||||
self._driver, 'check_update_share_server_security_service',
|
||||
mock.Mock(return_value=True))
|
||||
mock_get_container_name = self.mock_object(
|
||||
self._driver, '_get_container_name',
|
||||
mock.Mock(return_value=fake_container_name))
|
||||
mock_setup = self.mock_object(self._driver, 'setup_security_services')
|
||||
mock_update_sec_service = self.mock_object(
|
||||
self._driver.security_service_helper, 'update_security_service')
|
||||
|
||||
self._driver.update_share_server_security_service(
|
||||
self._context, share_server, network_info, share_instances,
|
||||
share_instance_access_rules, new_security_service,
|
||||
current_security_service=current_security_service)
|
||||
|
||||
mock_check_update.assert_called_once_with(
|
||||
self._context, share_server, network_info, share_instances,
|
||||
share_instance_access_rules, new_security_service,
|
||||
current_security_service=current_security_service
|
||||
)
|
||||
mock_get_container_name.assert_called_once_with(share_server['id'])
|
||||
if with_current_service:
|
||||
mock_update_sec_service.assert_called_once_with(
|
||||
fake_container_name, current_security_service,
|
||||
new_security_service)
|
||||
else:
|
||||
mock_setup.assert_called_once_with(
|
||||
fake_container_name, [new_security_service])
|
||||
|
||||
def test_update_share_server_security_service_not_supported(self):
|
||||
new_security_service = db_utils.create_security_service()
|
||||
current_security_service = db_utils.create_security_service()
|
||||
share_server = db_utils.create_share_server()
|
||||
share_instances = []
|
||||
share_instance_access_rules = []
|
||||
network_info = {}
|
||||
|
||||
mock_check_update = self.mock_object(
|
||||
self._driver, 'check_update_share_server_security_service',
|
||||
mock.Mock(return_value=False))
|
||||
|
||||
self.assertRaises(
|
||||
exception.ManilaException,
|
||||
self._driver.update_share_server_security_service,
|
||||
self._context, share_server, network_info, share_instances,
|
||||
share_instance_access_rules, new_security_service,
|
||||
current_security_service=current_security_service)
|
||||
|
||||
mock_check_update.assert_called_once_with(
|
||||
self._context, share_server, network_info, share_instances,
|
||||
share_instance_access_rules, new_security_service,
|
||||
current_security_service=current_security_service)
|
||||
|
@ -0,0 +1,173 @@
|
||||
# Copyright (c) 2021 NetApp, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Unit tests for the Security Service helper module."""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
|
||||
from manila import exception
|
||||
from manila.share import configuration
|
||||
from manila.share.drivers.container import security_service_helper
|
||||
from manila import test
|
||||
from manila.tests import db_utils
|
||||
|
||||
|
||||
INVALID_CREDENTIALS_EXIT_CODE = 49
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class SecurityServiceHelperTestCase(test.TestCase):
|
||||
"""Tests DockerExecHelper"""
|
||||
|
||||
def setUp(self):
|
||||
super(SecurityServiceHelperTestCase, self).setUp()
|
||||
self.fake_conf = configuration.Configuration(None)
|
||||
self.fake_conf.container_image_name = "fake_image"
|
||||
self.fake_conf.container_volume_mount_path = "/tmp/shares"
|
||||
self.security_service_helper = (
|
||||
security_service_helper.SecurityServiceHelper(
|
||||
configuration=self.fake_conf))
|
||||
|
||||
def test_setup_security_service(self):
|
||||
share_server = db_utils.create_share_server()
|
||||
security_service = db_utils.create_security_service()
|
||||
|
||||
mock_ldap_bind = self.mock_object(
|
||||
self.security_service_helper, 'ldap_bind')
|
||||
|
||||
self.security_service_helper.setup_security_service(
|
||||
share_server['id'], security_service)
|
||||
|
||||
mock_ldap_bind.assert_called_once_with(
|
||||
share_server['id'], security_service)
|
||||
|
||||
def test_update_security_service(self):
|
||||
share_server = db_utils.create_share_server()
|
||||
current_security_service = db_utils.create_security_service()
|
||||
new_security_service = db_utils.create_security_service()
|
||||
|
||||
mock_ldap_bind = self.mock_object(
|
||||
self.security_service_helper, 'ldap_bind')
|
||||
|
||||
self.security_service_helper.update_security_service(
|
||||
share_server['id'], current_security_service, new_security_service)
|
||||
|
||||
mock_ldap_bind.assert_called_once_with(
|
||||
share_server['id'], new_security_service)
|
||||
|
||||
def _setup_test_ldap_bind_tests(self):
|
||||
share_server = db_utils.create_security_service()
|
||||
security_service = db_utils.create_security_service()
|
||||
ldap_get_info = {
|
||||
'ss_password': security_service['password'],
|
||||
'ss_user': security_service['user']
|
||||
}
|
||||
expected_cmd = [
|
||||
"docker", "exec", "%s" % share_server['id'], "ldapwhoami", "-x",
|
||||
"-H", "ldap://localhost:389", "-D",
|
||||
"cn=%s,dc=example,dc=com" % ldap_get_info[
|
||||
"ss_user"],
|
||||
"-w", "%s" % ldap_get_info["ss_password"]]
|
||||
|
||||
return share_server, security_service, ldap_get_info, expected_cmd
|
||||
|
||||
def test_ldap_bind(self):
|
||||
share_server, security_service, ldap_get_info, expected_cmd = (
|
||||
self._setup_test_ldap_bind_tests())
|
||||
|
||||
mock_ldap_get_info = self.mock_object(
|
||||
self.security_service_helper, 'ldap_get_info',
|
||||
mock.Mock(return_value=ldap_get_info))
|
||||
mock_ldap_retry_operation = self.mock_object(
|
||||
self.security_service_helper, 'ldap_retry_operation')
|
||||
|
||||
self.security_service_helper.ldap_bind(
|
||||
share_server['id'], security_service)
|
||||
|
||||
mock_ldap_get_info.assert_called_once_with(security_service)
|
||||
mock_ldap_retry_operation.assert_called_once_with(expected_cmd,
|
||||
run_as_root=True)
|
||||
|
||||
def test_ldap_get_info(self):
|
||||
security_service = db_utils.create_security_service()
|
||||
expected_ldap_get_info = {
|
||||
'ss_password': security_service['password'],
|
||||
'ss_user': security_service['user']
|
||||
}
|
||||
|
||||
ldap_get_info = self.security_service_helper.ldap_get_info(
|
||||
security_service)
|
||||
|
||||
self.assertEqual(expected_ldap_get_info, ldap_get_info)
|
||||
|
||||
@ddt.data(
|
||||
{'type': 'ldap'},
|
||||
{'user': 'fake_user'},
|
||||
{'password': 'fake_password'},
|
||||
)
|
||||
def test_ldap_get_info_exception(self, sec_service_data):
|
||||
self.assertRaises(
|
||||
exception.ShareBackendException,
|
||||
self.security_service_helper.ldap_get_info,
|
||||
sec_service_data
|
||||
)
|
||||
|
||||
def test_ldap_retry_operation(self):
|
||||
mock_cmd = ["command", "to", "be", "executed"]
|
||||
|
||||
mock_execute = self.mock_object(self.security_service_helper,
|
||||
'_execute')
|
||||
|
||||
self.security_service_helper.ldap_retry_operation(mock_cmd,
|
||||
run_as_root=True)
|
||||
|
||||
mock_execute.assert_called_once_with(*mock_cmd, run_as_root=True)
|
||||
|
||||
def test_ldap_retry_operation_timeout(self):
|
||||
mock_cmd = ["command", "to", "be", "executed"]
|
||||
|
||||
mock_execute = self.mock_object(
|
||||
self.security_service_helper, '_execute',
|
||||
mock.Mock(
|
||||
side_effect=exception.ProcessExecutionError(exit_code=1)))
|
||||
|
||||
self.assertRaises(
|
||||
exception.ShareBackendException,
|
||||
self.security_service_helper.ldap_retry_operation,
|
||||
mock_cmd,
|
||||
run_as_root=False,
|
||||
timeout=10)
|
||||
|
||||
mock_execute.assert_has_calls([
|
||||
mock.call(*mock_cmd, run_as_root=False),
|
||||
mock.call(*mock_cmd, run_as_root=False)])
|
||||
|
||||
def test_ldap_retry_operation_invalid_credential(self):
|
||||
mock_cmd = ["command", "to", "be", "executed"]
|
||||
|
||||
mock_execute = self.mock_object(
|
||||
self.security_service_helper, '_execute',
|
||||
mock.Mock(
|
||||
side_effect=exception.ProcessExecutionError(
|
||||
exit_code=49)))
|
||||
|
||||
self.assertRaises(
|
||||
exception.ShareBackendException,
|
||||
self.security_service_helper.ldap_retry_operation,
|
||||
mock_cmd,
|
||||
run_as_root=False)
|
||||
|
||||
mock_execute.assert_called_once_with(*mock_cmd, run_as_root=False)
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
The Container Driver is now able to handle LDAP security services
|
||||
configuration while setting up share servers. Also, the Container Driver
|
||||
allows adding or updating ``LDAP`` security services to in use share
|
||||
networks.
|
Loading…
Reference in New Issue
Block a user