Add WinRM helper
This patch set adds a WinRM helper, which will be used for managing Windows Service Instances. This will make use of the pywinrm library. Password as well as certificate based authentication will be available. Co-Authored-By: Alin Balutoiu <abalutoiu@cloudbasesolutions.com> Change-Id: I6f0ca85c7e4ed35301fff4714e65d54f1fcc29a6 Partially-implements: blueprint windows-smb-support
This commit is contained in:
parent
214becb9c2
commit
e622b47880
|
@ -124,6 +124,7 @@ _global_opt_lists = [
|
|||
manila.share.drivers.service_instance.common_opts,
|
||||
manila.share.drivers.service_instance.no_share_servers_handling_mode_opts,
|
||||
manila.share.drivers.service_instance.share_servers_handling_mode_opts,
|
||||
manila.share.drivers.windows.winrm_helper.winrm_opts,
|
||||
manila.share.drivers.zfssa.zfssashare.ZFSSA_OPTS,
|
||||
manila.share.manager.share_manager_opts,
|
||||
manila.volume._volume_opts,
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
# Copyright (c) 2015 Cloudbase Solutions SRL
|
||||
# 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.
|
||||
|
||||
import base64
|
||||
import importlib
|
||||
import time
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
winrm_opts = [
|
||||
cfg.IntOpt(
|
||||
'winrm_conn_timeout',
|
||||
default=60,
|
||||
help='WinRM connection timeout.'),
|
||||
cfg.IntOpt(
|
||||
'winrm_operation_timeout',
|
||||
default=60,
|
||||
help='WinRM operation timeout.'),
|
||||
cfg.IntOpt(
|
||||
'winrm_retry_count',
|
||||
default=3,
|
||||
help='WinRM retry count.'),
|
||||
cfg.IntOpt(
|
||||
'winrm_retry_interval',
|
||||
default=5,
|
||||
help='WinRM retry interval in seconds'),
|
||||
]
|
||||
|
||||
CONF.register_opts(winrm_opts)
|
||||
|
||||
DEFAULT_PORT_HTTP = 5985
|
||||
DEFAULT_PORT_HTTPS = 5986
|
||||
|
||||
TRANSPORT_PLAINTEXT = 'plaintext'
|
||||
TRANSPORT_SSL = 'ssl'
|
||||
|
||||
winrm = None
|
||||
|
||||
|
||||
def setup_winrm():
|
||||
global winrm
|
||||
if not winrm:
|
||||
try:
|
||||
winrm = importlib.import_module('winrm')
|
||||
except ImportError:
|
||||
raise exception.ShareBackendException(
|
||||
_("PyWinrm is not installed"))
|
||||
|
||||
|
||||
class WinRMHelper(object):
|
||||
def __init__(self, configuration=None):
|
||||
if configuration:
|
||||
configuration.append_config_values(winrm_opts)
|
||||
self._config = configuration
|
||||
else:
|
||||
self._config = CONF
|
||||
|
||||
setup_winrm()
|
||||
|
||||
def _get_conn(self, server):
|
||||
auth = self._get_auth(server)
|
||||
conn = WinRMConnection(
|
||||
ip=server['ip'],
|
||||
conn_timeout=self._config.winrm_conn_timeout,
|
||||
operation_timeout=self._config.winrm_operation_timeout,
|
||||
**auth)
|
||||
return conn
|
||||
|
||||
def execute(self, server, command, check_exit_code=True,
|
||||
retry=True):
|
||||
conn = self._get_conn(server)
|
||||
|
||||
attempts = 1
|
||||
if retry and self._config.winrm_retry_count:
|
||||
attempts += self._config.winrm_retry_count
|
||||
|
||||
while attempts:
|
||||
try:
|
||||
return self._execute(conn, command, check_exit_code)
|
||||
except Exception as ex:
|
||||
attempts -= 1
|
||||
LOG.debug("Command execution failed. Exception: %(ex)s "
|
||||
"Remaining attempts: %(attempts)s.",
|
||||
dict(ex=ex,
|
||||
attempts=attempts))
|
||||
if not attempts:
|
||||
raise
|
||||
else:
|
||||
time.sleep(self._config.winrm_retry_interval)
|
||||
|
||||
def _execute(self, conn, command, check_exit_code=True):
|
||||
parsed_cmd, sanitized_cmd = self._parse_command(command)
|
||||
|
||||
LOG.debug("Executing command: %s", sanitized_cmd)
|
||||
(stdout, stderr, exit_code) = conn.execute(parsed_cmd)
|
||||
|
||||
sanitized_stdout = strutils.mask_password(stdout)
|
||||
sanitized_stderr = strutils.mask_password(stderr)
|
||||
LOG.debug("Executed command: %(cmd)s. Stdout: %(stdout)s. "
|
||||
"Stderr: %(stderr)s. Exit code %(exit_code)s",
|
||||
dict(cmd=sanitized_cmd, stdout=sanitized_stdout,
|
||||
stderr=sanitized_stderr, exit_code=exit_code))
|
||||
|
||||
if check_exit_code and exit_code != 0:
|
||||
raise processutils.ProcessExecutionError(stdout=sanitized_stdout,
|
||||
stderr=sanitized_stderr,
|
||||
exit_code=exit_code,
|
||||
cmd=sanitized_cmd)
|
||||
return (stdout, stderr)
|
||||
|
||||
def _parse_command(self, command):
|
||||
if isinstance(command, list) or isinstance(command, tuple):
|
||||
command = " ".join([six.text_type(c) for c in command])
|
||||
|
||||
sanitized_cmd = strutils.mask_password(command)
|
||||
|
||||
b64_command = base64.b64encode(command.encode("utf_16_le"))
|
||||
command = ("powershell.exe -ExecutionPolicy RemoteSigned "
|
||||
"-NonInteractive -EncodedCommand %s" % b64_command)
|
||||
return command, sanitized_cmd
|
||||
|
||||
def _get_auth(self, server):
|
||||
auth = {'username': server['username']}
|
||||
|
||||
if server['use_cert_auth']:
|
||||
auth['cert_pem_path'] = server['cert_pem_path']
|
||||
auth['cert_key_pem_path'] = server['cert_key_pem_path']
|
||||
else:
|
||||
auth['password'] = server['password']
|
||||
return auth
|
||||
|
||||
|
||||
class WinRMConnection(object):
|
||||
_URL_TEMPLATE = '%(protocol)s://%(ip)s:%(port)s/wsman'
|
||||
|
||||
def __init__(self, ip=None, port=None, use_ssl=False,
|
||||
transport=None, username=None, password=None,
|
||||
cert_pem_path=None, cert_key_pem_path=None,
|
||||
operation_timeout=None, conn_timeout=None):
|
||||
setup_winrm()
|
||||
|
||||
use_cert = bool(cert_pem_path and cert_key_pem_path)
|
||||
transport = (TRANSPORT_SSL
|
||||
if use_cert else TRANSPORT_PLAINTEXT)
|
||||
|
||||
_port = port or self._get_default_port(use_cert)
|
||||
_url = self._get_url(ip, _port, use_cert)
|
||||
|
||||
self._conn = winrm.protocol.Protocol(
|
||||
endpoint=_url, transport=transport,
|
||||
username=username, password=password,
|
||||
cert_pem=cert_pem_path, cert_key_pem=cert_key_pem_path)
|
||||
self._conn.transport.timeout = conn_timeout
|
||||
self._conn.set_timeout(operation_timeout)
|
||||
|
||||
def _get_default_port(self, use_ssl):
|
||||
port = (DEFAULT_PORT_HTTPS
|
||||
if use_ssl else DEFAULT_PORT_HTTP)
|
||||
return port
|
||||
|
||||
def _get_url(self, ip, port, use_ssl):
|
||||
if not ip:
|
||||
err_msg = _("No IP provided.")
|
||||
raise exception.ShareBackendException(msg=err_msg)
|
||||
|
||||
protocol = 'https' if use_ssl else 'http'
|
||||
return self._URL_TEMPLATE % {'protocol': protocol,
|
||||
'ip': ip,
|
||||
'port': port}
|
||||
|
||||
def execute(self, cmd):
|
||||
shell_id = None
|
||||
cmd_id = None
|
||||
|
||||
try:
|
||||
shell_id = self._conn.open_shell()
|
||||
|
||||
cmd_id = self._conn.run_command(shell_id, cmd)
|
||||
|
||||
(stdout,
|
||||
stderr,
|
||||
exit_code) = self._conn.get_command_output(shell_id, cmd_id)
|
||||
finally:
|
||||
if cmd_id:
|
||||
self._conn.cleanup_command(shell_id, cmd_id)
|
||||
if shell_id:
|
||||
self._conn.close_shell(shell_id)
|
||||
|
||||
return (stdout, stderr, exit_code)
|
|
@ -0,0 +1,298 @@
|
|||
# Copyright (c) 2015 Cloudbase Solutions SRL
|
||||
# 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.
|
||||
|
||||
import mock
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_utils import strutils
|
||||
|
||||
from manila import exception
|
||||
from manila.share.drivers.windows import winrm_helper
|
||||
from manila import test
|
||||
|
||||
|
||||
class WinRMHelperTestCase(test.TestCase):
|
||||
_FAKE_SERVER = {'ip': mock.sentinel.ip}
|
||||
|
||||
@mock.patch.object(winrm_helper, 'setup_winrm')
|
||||
def setUp(self, mock_setup_winrm):
|
||||
self._winrm = winrm_helper.WinRMHelper()
|
||||
super(WinRMHelperTestCase, self).setUp()
|
||||
|
||||
@mock.patch.object(winrm_helper.WinRMHelper, '_get_auth')
|
||||
@mock.patch.object(winrm_helper, 'WinRMConnection')
|
||||
def test_get_conn(self, mock_conn_cls, mock_get_auth):
|
||||
mock_auth = {'mock_auth_key': mock.sentinel.auth_opt}
|
||||
mock_get_auth.return_value = mock_auth
|
||||
|
||||
conn = self._winrm._get_conn(self._FAKE_SERVER)
|
||||
|
||||
mock_get_auth.assert_called_once_with(self._FAKE_SERVER)
|
||||
mock_conn_cls.assert_called_once_with(
|
||||
ip=self._FAKE_SERVER['ip'],
|
||||
conn_timeout=self._winrm._config.winrm_conn_timeout,
|
||||
operation_timeout=self._winrm._config.winrm_operation_timeout,
|
||||
**mock_auth)
|
||||
self.assertEqual(mock_conn_cls.return_value, conn)
|
||||
|
||||
@mock.patch('time.sleep')
|
||||
@mock.patch.object(winrm_helper.WinRMHelper, '_get_conn')
|
||||
@mock.patch.object(winrm_helper.WinRMHelper, '_execute')
|
||||
def _test_execute(self, mock_execute, mock_get_conn, mock_sleep,
|
||||
expected_try_count=1, retry=True, retry_count=1,
|
||||
side_effect=None, expected_exception=None):
|
||||
self.flags(winrm_retry_count=retry_count)
|
||||
mock_execute.side_effect = side_effect
|
||||
|
||||
if not expected_exception:
|
||||
result = self._winrm.execute(self._FAKE_SERVER,
|
||||
mock.sentinel.command,
|
||||
mock.sentinel.check_exit_code,
|
||||
retry=retry)
|
||||
self.assertEqual(mock.sentinel.result, result)
|
||||
else:
|
||||
self.assertRaises(expected_exception,
|
||||
self._winrm.execute,
|
||||
self._FAKE_SERVER,
|
||||
mock.sentinel.command,
|
||||
mock.sentinel.check_exit_code,
|
||||
retry=retry)
|
||||
|
||||
mock_get_conn.assert_called_once_with(self._FAKE_SERVER)
|
||||
mock_execute.assert_has_calls(
|
||||
[mock.call(mock_get_conn.return_value,
|
||||
mock.sentinel.command,
|
||||
mock.sentinel.check_exit_code)] * expected_try_count)
|
||||
mock_sleep.assert_has_calls(
|
||||
[mock.call(self._winrm._config.winrm_retry_interval)] *
|
||||
(expected_try_count - 1))
|
||||
|
||||
def test_execute(self):
|
||||
self._test_execute(side_effect=[mock.sentinel.result])
|
||||
|
||||
def test_execute_exception_without_retry(self):
|
||||
self._test_execute(retry=False,
|
||||
side_effect=Exception,
|
||||
expected_exception=Exception)
|
||||
|
||||
def test_execute_exception_after_retry(self):
|
||||
retry_count = 2
|
||||
self._test_execute(side_effect=Exception,
|
||||
expected_exception=Exception,
|
||||
retry_count=retry_count,
|
||||
expected_try_count=retry_count + 1)
|
||||
|
||||
def test_execute_success_after_retry(self):
|
||||
retry_count = 2
|
||||
side_effect = (Exception, mock.sentinel.result)
|
||||
self._test_execute(side_effect=side_effect,
|
||||
expected_try_count=2,
|
||||
retry_count=retry_count)
|
||||
|
||||
@mock.patch.object(strutils, 'mask_password')
|
||||
@mock.patch.object(winrm_helper.WinRMHelper, '_parse_command')
|
||||
def _test_execute_helper(self, mock_parse_command, mock_mask_password,
|
||||
check_exit_code=True, exit_code=0):
|
||||
mock_parse_command.return_value = (mock.sentinel.parsed_cmd,
|
||||
mock.sentinel.sanitized_cmd)
|
||||
mock_conn = mock.Mock()
|
||||
mock_conn.execute.return_value = (mock.sentinel.stdout,
|
||||
mock.sentinel.stderr,
|
||||
exit_code)
|
||||
|
||||
if exit_code == 0 or not check_exit_code:
|
||||
result = self._winrm._execute(mock_conn,
|
||||
mock.sentinel.command,
|
||||
check_exit_code=check_exit_code)
|
||||
expected_result = (mock.sentinel.stdout, mock.sentinel.stderr)
|
||||
self.assertEqual(expected_result, result)
|
||||
else:
|
||||
self.assertRaises(processutils.ProcessExecutionError,
|
||||
self._winrm._execute,
|
||||
mock_conn,
|
||||
mock.sentinel.command,
|
||||
check_exit_code=check_exit_code)
|
||||
|
||||
mock_parse_command.assert_called_once_with(mock.sentinel.command)
|
||||
mock_conn.execute.assert_called_once_with(mock.sentinel.parsed_cmd)
|
||||
mock_mask_password.assert_has_calls([mock.call(mock.sentinel.stdout),
|
||||
mock.call(mock.sentinel.stderr)])
|
||||
|
||||
def test_execute_helper(self):
|
||||
self._test_execute_helper()
|
||||
|
||||
def test_execute_helper_exception(self):
|
||||
self._test_execute_helper(exit_code=1)
|
||||
|
||||
def test_execute_helper_exception_ignored(self):
|
||||
self._test_execute_helper(exit_code=1, check_exit_code=False)
|
||||
|
||||
@mock.patch('base64.b64encode')
|
||||
@mock.patch.object(strutils, 'mask_password')
|
||||
def test_parse_command(self, mock_mask_password, mock_base64):
|
||||
mock_mask_password.return_value = mock.sentinel.sanitized_cmd
|
||||
mock_base64.return_value = mock.sentinel.encoded_string
|
||||
|
||||
cmd = ('Get-Disk', '-Number', 1)
|
||||
result = self._winrm._parse_command(cmd)
|
||||
|
||||
joined_cmd = 'Get-Disk -Number 1'
|
||||
expected_command = ("powershell.exe -ExecutionPolicy RemoteSigned "
|
||||
"-NonInteractive -EncodedCommand %s" %
|
||||
mock.sentinel.encoded_string)
|
||||
expected_result = expected_command, mock.sentinel.sanitized_cmd
|
||||
|
||||
mock_mask_password.assert_called_once_with(joined_cmd)
|
||||
mock_base64.assert_called_once_with(joined_cmd.encode("utf_16_le"))
|
||||
self.assertEqual(expected_result, result)
|
||||
|
||||
def _test_get_auth(self, use_cert_auth=False):
|
||||
mock_server = {'use_cert_auth': use_cert_auth,
|
||||
'cert_pem_path': mock.sentinel.pem_path,
|
||||
'cert_key_pem_path': mock.sentinel.key_path,
|
||||
'username': mock.sentinel.username,
|
||||
'password': mock.sentinel.password}
|
||||
|
||||
result = self._winrm._get_auth(mock_server)
|
||||
|
||||
expected_result = {'username': mock_server['username']}
|
||||
if use_cert_auth:
|
||||
expected_result['cert_pem_path'] = mock_server['cert_pem_path']
|
||||
expected_result['cert_key_pem_path'] = (
|
||||
mock_server['cert_key_pem_path'])
|
||||
else:
|
||||
expected_result['password'] = mock_server['password']
|
||||
|
||||
self.assertEqual(expected_result, result)
|
||||
|
||||
def test_get_auth_using_certificates(self):
|
||||
self._test_get_auth(use_cert_auth=True)
|
||||
|
||||
def test_get_auth_using_password(self):
|
||||
self._test_get_auth()
|
||||
|
||||
|
||||
class WinRMConnectionTestCase(test.TestCase):
|
||||
@mock.patch.object(winrm_helper, 'setup_winrm')
|
||||
@mock.patch.object(winrm_helper, 'winrm')
|
||||
@mock.patch.object(winrm_helper.WinRMConnection, '_get_url')
|
||||
@mock.patch.object(winrm_helper.WinRMConnection, '_get_default_port')
|
||||
def setUp(self, mock_get_port, mock_get_url, mock_winrm,
|
||||
mock_setup_winrm):
|
||||
self._winrm = winrm_helper.WinRMConnection()
|
||||
self._mock_conn = mock_winrm.protocol.Protocol.return_value
|
||||
super(WinRMConnectionTestCase, self).setUp()
|
||||
|
||||
@mock.patch.object(winrm_helper, 'setup_winrm')
|
||||
@mock.patch.object(winrm_helper, 'winrm')
|
||||
@mock.patch.object(winrm_helper.WinRMConnection, '_get_url')
|
||||
@mock.patch.object(winrm_helper.WinRMConnection, '_get_default_port')
|
||||
def test_init_conn(self, mock_get_port, mock_get_url, mock_winrm,
|
||||
mock_setup_winrm):
|
||||
# certificates are passed so we expect cert auth to be used
|
||||
cert_auth = True
|
||||
winrm_conn = winrm_helper.WinRMConnection(
|
||||
ip=mock.sentinel.ip, username=mock.sentinel.username,
|
||||
password=mock.sentinel.password,
|
||||
cert_pem_path=mock.sentinel.cert_pem_path,
|
||||
cert_key_pem_path=mock.sentinel.cert_key_pem_path,
|
||||
operation_timeout=mock.sentinel.operation_timeout,
|
||||
conn_timeout=mock.sentinel.conn_timeout)
|
||||
|
||||
mock_get_port.assert_called_once_with(cert_auth)
|
||||
mock_get_url.assert_called_once_with(mock.sentinel.ip,
|
||||
mock_get_port.return_value,
|
||||
cert_auth)
|
||||
mock_winrm.protocol.Protocol.assert_called_once_with(
|
||||
endpoint=mock_get_url.return_value,
|
||||
transport=winrm_helper.TRANSPORT_SSL,
|
||||
username=mock.sentinel.username,
|
||||
password=mock.sentinel.password,
|
||||
cert_pem=mock.sentinel.cert_pem_path,
|
||||
cert_key_pem=mock.sentinel.cert_key_pem_path)
|
||||
self.assertEqual(mock_winrm.protocol.Protocol.return_value,
|
||||
winrm_conn._conn)
|
||||
self.assertEqual(mock.sentinel.conn_timeout,
|
||||
winrm_conn._conn.transport.timeout)
|
||||
winrm_conn._conn.set_timeout.assert_called_once_with(
|
||||
mock.sentinel.operation_timeout)
|
||||
|
||||
def test_get_default_port_https(self):
|
||||
port = self._winrm._get_default_port(use_ssl=True)
|
||||
self.assertEqual(winrm_helper.DEFAULT_PORT_HTTPS, port)
|
||||
|
||||
def test_get_default_port_http(self):
|
||||
port = self._winrm._get_default_port(use_ssl=False)
|
||||
self.assertEqual(winrm_helper.DEFAULT_PORT_HTTP, port)
|
||||
|
||||
def _test_get_url(self, ip=None, use_ssl=True):
|
||||
if not ip:
|
||||
self.assertRaises(exception.ShareBackendException,
|
||||
self._winrm._get_url,
|
||||
ip=ip,
|
||||
port=mock.sentinel.port,
|
||||
use_ssl=use_ssl)
|
||||
else:
|
||||
url = self._winrm._get_url(ip=ip,
|
||||
port=mock.sentinel.port,
|
||||
use_ssl=use_ssl)
|
||||
expected_protocol = 'https' if use_ssl else 'http'
|
||||
expected_url = self._winrm._URL_TEMPLATE % dict(
|
||||
protocol=expected_protocol,
|
||||
port=mock.sentinel.port,
|
||||
ip=ip)
|
||||
self.assertEqual(expected_url, url)
|
||||
|
||||
def test_get_url_using_ssl(self):
|
||||
self._test_get_url(ip=mock.sentinel.ip)
|
||||
|
||||
def test_get_url_using_plaintext(self):
|
||||
self._test_get_url(ip=mock.sentinel.ip, use_ssl=False)
|
||||
|
||||
def test_get_url_missing_ip(self):
|
||||
self._test_get_url()
|
||||
|
||||
def _test_execute(self, get_output_exception=None):
|
||||
self._mock_conn.open_shell.return_value = mock.sentinel.shell_id
|
||||
self._mock_conn.run_command.return_value = mock.sentinel.cmd_id
|
||||
|
||||
command_output = (mock.sentinel.stdout,
|
||||
mock.sentinel.stderr,
|
||||
mock.sentinel.exit_code)
|
||||
if get_output_exception:
|
||||
self._mock_conn.get_command_output.side_effect = (
|
||||
get_output_exception)
|
||||
self.assertRaises(
|
||||
get_output_exception,
|
||||
self._winrm.execute,
|
||||
mock.sentinel.cmd)
|
||||
else:
|
||||
self._mock_conn.get_command_output.return_value = command_output
|
||||
result = self._winrm.execute(mock.sentinel.cmd)
|
||||
self.assertEqual(command_output, result)
|
||||
|
||||
self._mock_conn.open_shell.assert_called_once_with()
|
||||
self._mock_conn.run_command.assert_called_once_with(
|
||||
mock.sentinel.shell_id, mock.sentinel.cmd)
|
||||
|
||||
self._mock_conn.cleanup_command.assert_called_once_with(
|
||||
mock.sentinel.shell_id, mock.sentinel.cmd_id)
|
||||
self._mock_conn.close_shell.assert_called_once_with(
|
||||
mock.sentinel.shell_id)
|
||||
|
||||
def test_execute(self):
|
||||
self._test_execute()
|
||||
|
||||
def test_execute_exception(self):
|
||||
self._test_execute(get_output_exception=Exception)
|
Loading…
Reference in New Issue