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
This commit is contained in:
Eric Harney 2014-07-25 15:59:40 -04:00
parent 078f3ea1bf
commit 75ef446fef
8 changed files with 198 additions and 162 deletions

120
cinder/ssh_utils.py Normal file
View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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]

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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()