From 75ef446fef63320e9c1ed4a04e59ffbbb62b5cef Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Fri, 25 Jul 2014 15:59:40 -0400 Subject: [PATCH] Move SSHPool into ssh_utils.py utils.py is loaded by almost everything, but SSH code and imports (paramiko) are only needed by certain drivers. Split this into a separate file to reduce overhead (and things that can break) for commands like cinder-manage. Partial-Bug: #1348787 Change-Id: I46896f2bd1fd0de2aedde8e87e255398e5bc3171 --- cinder/ssh_utils.py | 120 ++++++++++++++++++ cinder/tests/test_huawei_t_dorado.py | 8 +- cinder/tests/test_utils.py | 57 +++++---- cinder/utils.py | 92 -------------- cinder/volume/drivers/eqlx.py | 18 +-- cinder/volume/drivers/huawei/ssh_common.py | 4 +- cinder/volume/drivers/san/san.py | 18 +-- .../brocade/brcd_fc_zone_client_cli.py | 43 ++++--- 8 files changed, 198 insertions(+), 162 deletions(-) create mode 100644 cinder/ssh_utils.py diff --git a/cinder/ssh_utils.py b/cinder/ssh_utils.py new file mode 100644 index 000000000..bbeb030db --- /dev/null +++ b/cinder/ssh_utils.py @@ -0,0 +1,120 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Justin Santa Barbara +# Copyright 2014 Red Hat, 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. + +"""Utilities related to SSH connection management.""" + +import os.path + +from eventlet import pools +import paramiko + +from cinder import exception +from cinder.openstack.common.gettextutils import _ +from cinder.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +class SSHPool(pools.Pool): + """A simple eventlet pool to hold ssh connections.""" + + def __init__(self, ip, port, conn_timeout, login, password=None, + privatekey=None, *args, **kwargs): + self.ip = ip + self.port = port + self.login = login + self.password = password + self.conn_timeout = conn_timeout if conn_timeout else None + self.privatekey = privatekey + if 'missing_key_policy' in kwargs.keys(): + self.missing_key_policy = kwargs.pop('missing_key_policy') + else: + self.missing_key_policy = paramiko.AutoAddPolicy() + if 'hosts_key_file' in kwargs.keys(): + self.hosts_key_file = kwargs.pop('hosts_key_file') + else: + self.hosts_key_file = None + super(SSHPool, self).__init__(*args, **kwargs) + + def create(self): + try: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(self.missing_key_policy) + if not self.hosts_key_file: + ssh.load_system_host_keys() + else: + ssh.load_host_keys(self.hosts_key_file) + if self.password: + ssh.connect(self.ip, + port=self.port, + username=self.login, + password=self.password, + timeout=self.conn_timeout) + elif self.privatekey: + pkfile = os.path.expanduser(self.privatekey) + privatekey = paramiko.RSAKey.from_private_key_file(pkfile) + ssh.connect(self.ip, + port=self.port, + username=self.login, + pkey=privatekey, + timeout=self.conn_timeout) + else: + msg = _("Specify a password or private_key") + raise exception.CinderException(msg) + + # Paramiko by default sets the socket timeout to 0.1 seconds, + # ignoring what we set through the sshclient. This doesn't help for + # keeping long lived connections. Hence we have to bypass it, by + # overriding it after the transport is initialized. We are setting + # the sockettimeout to None and setting a keepalive packet so that, + # the server will keep the connection open. All that does is send + # a keepalive packet every ssh_conn_timeout seconds. + if self.conn_timeout: + transport = ssh.get_transport() + transport.sock.settimeout(None) + transport.set_keepalive(self.conn_timeout) + return ssh + except Exception as e: + msg = _("Error connecting via ssh: %s") % e + LOG.error(msg) + raise paramiko.SSHException(msg) + + def get(self): + """Return an item from the pool, when one is available. + + This may cause the calling greenthread to block. Check if a + connection is active before returning it. + + For dead connections create and return a new connection. + """ + conn = super(SSHPool, self).get() + if conn: + if conn.get_transport().is_active(): + return conn + else: + conn.close() + return self.create() + + def remove(self, ssh): + """Close an ssh client and remove it from free_items.""" + ssh.close() + ssh = None + if ssh in self.free_items: + self.free_items.pop(ssh) + if self.current_size > 0: + self.current_size -= 1 diff --git a/cinder/tests/test_huawei_t_dorado.py b/cinder/tests/test_huawei_t_dorado.py index b9bdf175f..6f25f006e 100644 --- a/cinder/tests/test_huawei_t_dorado.py +++ b/cinder/tests/test_huawei_t_dorado.py @@ -30,8 +30,8 @@ from xml.etree import ElementTree as ET from cinder import context from cinder import exception +from cinder import ssh_utils from cinder import test -from cinder import utils from cinder.volume import configuration as conf from cinder.volume.drivers.huawei import huawei_utils from cinder.volume.drivers.huawei import HuaweiVolumeDriver @@ -1079,7 +1079,7 @@ class HuaweiTISCSIDriverTestCase(test.TestCase): self.configuration.append_config_values(mox.IgnoreArg()) self.stubs.Set(time, 'sleep', Fake_sleep) - self.stubs.Set(utils, 'SSHPool', FakeSSHPool) + self.stubs.Set(ssh_utils, 'SSHPool', FakeSSHPool) self.stubs.Set(ssh_common.TseriesCommon, '_change_file_mode', Fake_change_file_mode) self._init_driver() @@ -1473,7 +1473,7 @@ class HuaweiTFCDriverTestCase(test.TestCase): self.configuration.append_config_values(mox.IgnoreArg()) self.stubs.Set(time, 'sleep', Fake_sleep) - self.stubs.Set(utils, 'SSHPool', FakeSSHPool) + self.stubs.Set(ssh_utils, 'SSHPool', FakeSSHPool) self.stubs.Set(ssh_common.TseriesCommon, '_change_file_mode', Fake_change_file_mode) self._init_driver() @@ -1705,7 +1705,7 @@ class SSHMethodTestCase(test.TestCase): self.configuration.append_config_values(mox.IgnoreArg()) self.stubs.Set(time, 'sleep', Fake_sleep) - self.stubs.Set(utils, 'SSHPool', FakeSSHPool) + self.stubs.Set(ssh_utils, 'SSHPool', FakeSSHPool) self.stubs.Set(ssh_common.TseriesCommon, '_change_file_mode', Fake_change_file_mode) Curr_test[0] = 'T' diff --git a/cinder/tests/test_utils.py b/cinder/tests/test_utils.py index ff7b8c7ee..75a524848 100644 --- a/cinder/tests/test_utils.py +++ b/cinder/tests/test_utils.py @@ -31,6 +31,7 @@ from cinder.brick.initiator import linuxfc from cinder import exception from cinder.openstack.common import processutils as putils from cinder.openstack.common import timeutils +from cinder import ssh_utils from cinder import test from cinder import utils @@ -870,24 +871,24 @@ class SSHPoolTestCase(test.TestCase): mock_sshclient.return_value = FakeSSHClient() # create with customized setting - sshpool = utils.SSHPool("127.0.0.1", 22, 10, - "test", - password="test", - min_size=1, - max_size=1, - missing_key_policy=paramiko.RejectPolicy(), - hosts_key_file='dummy_host_keyfile') + sshpool = ssh_utils.SSHPool("127.0.0.1", 22, 10, + "test", + password="test", + min_size=1, + max_size=1, + missing_key_policy=paramiko.RejectPolicy(), + hosts_key_file='dummy_host_keyfile') with sshpool.item() as ssh: self.assertTrue(isinstance(ssh.get_policy(), paramiko.RejectPolicy)) self.assertEqual(ssh.hosts_key_file, 'dummy_host_keyfile') # create with default setting - sshpool = utils.SSHPool("127.0.0.1", 22, 10, - "test", - password="test", - min_size=1, - max_size=1) + sshpool = ssh_utils.SSHPool("127.0.0.1", 22, 10, + "test", + password="test", + min_size=1, + max_size=1) with sshpool.item() as ssh: self.assertTrue(isinstance(ssh.get_policy(), paramiko.AutoAddPolicy)) @@ -899,11 +900,11 @@ class SSHPoolTestCase(test.TestCase): mock_sshclient.return_value = FakeSSHClient() # create with password - sshpool = utils.SSHPool("127.0.0.1", 22, 10, - "test", - password="test", - min_size=1, - max_size=1) + sshpool = ssh_utils.SSHPool("127.0.0.1", 22, 10, + "test", + password="test", + min_size=1, + max_size=1) with sshpool.item() as ssh: first_id = ssh.id @@ -914,16 +915,16 @@ class SSHPoolTestCase(test.TestCase): mock_sshclient.connect.assert_called_once() # create with private key - sshpool = utils.SSHPool("127.0.0.1", 22, 10, - "test", - privatekey="test", - min_size=1, - max_size=1) + sshpool = ssh_utils.SSHPool("127.0.0.1", 22, 10, + "test", + privatekey="test", + min_size=1, + max_size=1) mock_sshclient.connect.assert_called_once() # attempt to create with no password or private key self.assertRaises(paramiko.SSHException, - utils.SSHPool, + ssh_utils.SSHPool, "127.0.0.1", 22, 10, "test", min_size=1, @@ -932,11 +933,11 @@ class SSHPoolTestCase(test.TestCase): @mock.patch('paramiko.SSHClient') def test_closed_reopend_ssh_connections(self, mock_sshclient): mock_sshclient.return_value = eval('FakeSSHClient')() - sshpool = utils.SSHPool("127.0.0.1", 22, 10, - "test", - password="test", - min_size=1, - max_size=4) + sshpool = ssh_utils.SSHPool("127.0.0.1", 22, 10, + "test", + password="test", + min_size=1, + max_size=4) with sshpool.item() as ssh: mock_sshclient.reset_mock() first_id = ssh.id diff --git a/cinder/utils.py b/cinder/utils.py index 0807e9464..440b634f9 100644 --- a/cinder/utils.py +++ b/cinder/utils.py @@ -31,9 +31,7 @@ import sys import tempfile from Crypto.Random import random -from eventlet import pools from oslo.config import cfg -import paramiko import six from xml.dom import minidom from xml.parsers import expat @@ -179,96 +177,6 @@ def create_channel(client, width, height): return channel -class SSHPool(pools.Pool): - """A simple eventlet pool to hold ssh connections.""" - - def __init__(self, ip, port, conn_timeout, login, password=None, - privatekey=None, *args, **kwargs): - self.ip = ip - self.port = port - self.login = login - self.password = password - self.conn_timeout = conn_timeout if conn_timeout else None - self.privatekey = privatekey - if 'missing_key_policy' in kwargs.keys(): - self.missing_key_policy = kwargs.pop('missing_key_policy') - else: - self.missing_key_policy = paramiko.AutoAddPolicy() - if 'hosts_key_file' in kwargs.keys(): - self.hosts_key_file = kwargs.pop('hosts_key_file') - else: - self.hosts_key_file = None - super(SSHPool, self).__init__(*args, **kwargs) - - def create(self): - try: - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(self.missing_key_policy) - if not self.hosts_key_file: - ssh.load_system_host_keys() - else: - ssh.load_host_keys(self.hosts_key_file) - if self.password: - ssh.connect(self.ip, - port=self.port, - username=self.login, - password=self.password, - timeout=self.conn_timeout) - elif self.privatekey: - pkfile = os.path.expanduser(self.privatekey) - privatekey = paramiko.RSAKey.from_private_key_file(pkfile) - ssh.connect(self.ip, - port=self.port, - username=self.login, - pkey=privatekey, - timeout=self.conn_timeout) - else: - msg = _("Specify a password or private_key") - raise exception.CinderException(msg) - - # Paramiko by default sets the socket timeout to 0.1 seconds, - # ignoring what we set through the sshclient. This doesn't help for - # keeping long lived connections. Hence we have to bypass it, by - # overriding it after the transport is initialized. We are setting - # the sockettimeout to None and setting a keepalive packet so that, - # the server will keep the connection open. All that does is send - # a keepalive packet every ssh_conn_timeout seconds. - if self.conn_timeout: - transport = ssh.get_transport() - transport.sock.settimeout(None) - transport.set_keepalive(self.conn_timeout) - return ssh - except Exception as e: - msg = _("Error connecting via ssh: %s") % e - LOG.error(msg) - raise paramiko.SSHException(msg) - - def get(self): - """Return an item from the pool, when one is available. - - This may cause the calling greenthread to block. Check if a - connection is active before returning it. - - For dead connections create and return a new connection. - """ - conn = super(SSHPool, self).get() - if conn: - if conn.get_transport().is_active(): - return conn - else: - conn.close() - return self.create() - - def remove(self, ssh): - """Close an ssh client and remove it from free_items.""" - ssh.close() - ssh = None - if ssh in self.free_items: - self.free_items.pop(ssh) - if self.current_size > 0: - self.current_size -= 1 - - def cinderdir(): import cinder return os.path.abspath(cinder.__file__).split('cinder/__init__.py')[0] diff --git a/cinder/volume/drivers/eqlx.py b/cinder/volume/drivers/eqlx.py index 6eec44de0..1030bc8f2 100644 --- a/cinder/volume/drivers/eqlx.py +++ b/cinder/volume/drivers/eqlx.py @@ -28,6 +28,7 @@ from cinder.openstack.common import excutils from cinder.openstack.common.gettextutils import _ from cinder.openstack.common import log as logging from cinder.openstack.common import processutils +from cinder import ssh_utils from cinder import utils from cinder.volume.drivers.san import SanISCSIDriver @@ -183,14 +184,15 @@ class DellEQLSanISCSIDriver(SanISCSIDriver): privatekey = self.configuration.san_private_key min_size = self.configuration.ssh_min_pool_conn max_size = self.configuration.ssh_max_pool_conn - self.sshpool = utils.SSHPool(self.configuration.san_ip, - self.configuration.san_ssh_port, - self.configuration.ssh_conn_timeout, - self.configuration.san_login, - password=password, - privatekey=privatekey, - min_size=min_size, - max_size=max_size) + self.sshpool = ssh_utils.SSHPool( + self.configuration.san_ip, + self.configuration.san_ssh_port, + self.configuration.ssh_conn_timeout, + self.configuration.san_login, + password=password, + privatekey=privatekey, + min_size=min_size, + max_size=max_size) try: total_attempts = attempts with self.sshpool.item() as ssh: diff --git a/cinder/volume/drivers/huawei/ssh_common.py b/cinder/volume/drivers/huawei/ssh_common.py index 80a374ae6..c9fe1e820 100644 --- a/cinder/volume/drivers/huawei/ssh_common.py +++ b/cinder/volume/drivers/huawei/ssh_common.py @@ -32,6 +32,7 @@ from cinder import exception from cinder.openstack.common import excutils from cinder.openstack.common.gettextutils import _ from cinder.openstack.common import log as logging +from cinder import ssh_utils from cinder import utils from cinder.volume.drivers.huawei import huawei_utils from cinder.volume import volume_types @@ -439,7 +440,8 @@ class TseriesCommon(): user = self.login_info['UserName'] pwd = self.login_info['UserPassword'] if not self.ssh_pool: - self.ssh_pool = utils.SSHPool(ip0, 22, 30, user, pwd, max_size=2) + self.ssh_pool = ssh_utils.SSHPool(ip0, 22, 30, user, pwd, + max_size=2) ssh_client = None while True: try: diff --git a/cinder/volume/drivers/san/san.py b/cinder/volume/drivers/san/san.py index 66810417b..a64361508 100644 --- a/cinder/volume/drivers/san/san.py +++ b/cinder/volume/drivers/san/san.py @@ -29,6 +29,7 @@ from cinder.openstack.common import excutils from cinder.openstack.common.gettextutils import _ from cinder.openstack.common import log as logging from cinder.openstack.common import processutils +from cinder import ssh_utils from cinder import utils from cinder.volume import driver @@ -109,14 +110,15 @@ class SanDriver(driver.VolumeDriver): privatekey = self.configuration.san_private_key min_size = self.configuration.ssh_min_pool_conn max_size = self.configuration.ssh_max_pool_conn - self.sshpool = utils.SSHPool(self.configuration.san_ip, - self.configuration.san_ssh_port, - self.configuration.ssh_conn_timeout, - self.configuration.san_login, - password=password, - privatekey=privatekey, - min_size=min_size, - max_size=max_size) + self.sshpool = ssh_utils.SSHPool( + self.configuration.san_ip, + self.configuration.san_ssh_port, + self.configuration.ssh_conn_timeout, + self.configuration.san_login, + password=password, + privatekey=privatekey, + min_size=min_size, + max_size=max_size) last_exception = None try: with self.sshpool.item() as ssh: diff --git a/cinder/zonemanager/drivers/brocade/brcd_fc_zone_client_cli.py b/cinder/zonemanager/drivers/brocade/brcd_fc_zone_client_cli.py index 839b215f1..fb072d3ec 100644 --- a/cinder/zonemanager/drivers/brocade/brcd_fc_zone_client_cli.py +++ b/cinder/zonemanager/drivers/brocade/brcd_fc_zone_client_cli.py @@ -31,6 +31,7 @@ from cinder.openstack.common import excutils from cinder.openstack.common.gettextutils import _ from cinder.openstack.common import log as logging from cinder.openstack.common import processutils +from cinder import ssh_utils from cinder import utils import cinder.zonemanager.drivers.brocade.fc_zone_constants as ZoneConstant @@ -378,13 +379,13 @@ class BrcdFCZoneClientCLI(object): command = ' '. join(cmd_list) if not self.sshpool: - self.sshpool = utils.SSHPool(self.switch_ip, - self.switch_port, - None, - self.switch_user, - self.switch_pwd, - min_size=1, - max_size=5) + self.sshpool = ssh_utils.SSHPool(self.switch_ip, + self.switch_port, + None, + self.switch_user, + self.switch_pwd, + min_size=1, + max_size=5) last_exception = None try: with self.sshpool.item() as ssh: @@ -424,13 +425,13 @@ class BrcdFCZoneClientCLI(object): command = ' '. join(cmd_list) if not self.sshpool: - self.sshpool = utils.SSHPool(self.switch_ip, - self.switch_port, - None, - self.switch_user, - self.switch_pwd, - min_size=1, - max_size=5) + self.sshpool = ssh_utils.SSHPool(self.switch_ip, + self.switch_port, + None, + self.switch_user, + self.switch_pwd, + min_size=1, + max_size=5) stdin, stdout, stderr = None, None, None LOG.debug("Executing command via ssh: %s" % command) last_exception = None @@ -499,13 +500,13 @@ class BrcdFCZoneClientCLI(object): command = ' '. join(cmd) stdout, stderr = None, None if not self.sshpool: - self.sshpool = utils.SSHPool(self.switch_ip, - self.switch_port, - None, - self.switch_user, - self.switch_pwd, - min_size=1, - max_size=5) + self.sshpool = ssh_utils.SSHPool(self.switch_ip, + self.switch_port, + None, + self.switch_user, + self.switch_pwd, + min_size=1, + max_size=5) with self.sshpool.item() as ssh: LOG.debug('Running cmd (SSH): %s' % command) channel = ssh.invoke_shell() -- 2.45.2