1# Copyright 2011 Justin Santa Barbara 2# All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may 5# not use this file except in compliance with the License. You may obtain 6# a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations 14# under the License. 15""" 16Default Driver for san-stored volumes. 17 18The unique thing about a SAN is that we don't expect that we can run the volume 19controller on the SAN hardware. We expect to access it over SSH or some API. 20""" 21 22import random 23 24from eventlet import greenthread 25from oslo_concurrency import processutils 26from oslo_config import cfg 27from oslo_log import log as logging 28from oslo_utils import excutils 29 30from cinder import exception 31from cinder.i18n import _ 32from cinder import ssh_utils 33from cinder import utils 34from cinder.volume import configuration 35from cinder.volume import driver 36 37LOG = logging.getLogger(__name__) 38 39san_opts = [ 40 cfg.BoolOpt('san_thin_provision', 41 default=True, 42 help='Use thin provisioning for SAN volumes?'), 43 cfg.StrOpt('san_ip', 44 default='', 45 help='IP address of SAN controller'), 46 cfg.StrOpt('san_login', 47 default='admin', 48 help='Username for SAN controller'), 49 cfg.StrOpt('san_password', 50 default='', 51 help='Password for SAN controller', 52 secret=True), 53 cfg.StrOpt('san_private_key', 54 default='', 55 help='Filename of private key to use for SSH authentication'), 56 cfg.StrOpt('san_clustername', 57 default='', 58 help='Cluster name to use for creating volumes'), 59 cfg.PortOpt('san_ssh_port', 60 default=22, 61 help='SSH port to use with SAN'), 62 cfg.PortOpt('san_api_port', 63 help='Port to use to access the SAN API'), 64 cfg.BoolOpt('san_is_local', 65 default=False, 66 help='Execute commands locally instead of over SSH; ' 67 'use if the volume service is running on the SAN device'), 68 cfg.IntOpt('ssh_conn_timeout', 69 default=30, 70 help="SSH connection timeout in seconds"), 71 cfg.IntOpt('ssh_min_pool_conn', 72 default=1, 73 help='Minimum ssh connections in the pool'), 74 cfg.IntOpt('ssh_max_pool_conn', 75 default=5, 76 help='Maximum ssh connections in the pool'), 77] 78 79CONF = cfg.CONF 80CONF.register_opts(san_opts, group=configuration.SHARED_CONF_GROUP) 81 82 83class SanDriver(driver.BaseVD): 84 """Base class for SAN-style storage volumes 85 86 A SAN-style storage value is 'different' because the volume controller 87 probably won't run on it, so we need to access is over SSH or another 88 remote protocol. 89 """ 90 91 def __init__(self, *args, **kwargs): 92 execute = kwargs.pop('execute', self.san_execute) 93 super(SanDriver, self).__init__(execute=execute, 94 *args, **kwargs) 95 self.configuration.append_config_values(san_opts) 96 self.run_local = self.configuration.san_is_local 97 self.sshpool = None 98 99 def san_execute(self, *cmd, **kwargs): 100 if self.run_local: 101 return utils.execute(*cmd, **kwargs) 102 else: 103 check_exit_code = kwargs.pop('check_exit_code', None) 104 return self._run_ssh(cmd, check_exit_code) 105 106 def _run_ssh(self, cmd_list, check_exit_code=True, attempts=1): 107 utils.check_ssh_injection(cmd_list) 108 command = ' '. join(cmd_list) 109 110 if not self.sshpool: 111 password = self.configuration.san_password 112 privatekey = self.configuration.san_private_key 113 min_size = self.configuration.ssh_min_pool_conn 114 max_size = self.configuration.ssh_max_pool_conn 115 self.sshpool = ssh_utils.SSHPool( 116 self.configuration.san_ip, 117 self.configuration.san_ssh_port, 118 self.configuration.ssh_conn_timeout, 119 self.configuration.san_login, 120 password=password, 121 privatekey=privatekey, 122 min_size=min_size, 123 max_size=max_size) 124 last_exception = None 125 try: 126 with self.sshpool.item() as ssh: 127 while attempts > 0: 128 attempts -= 1 129 try: 130 return processutils.ssh_execute( 131 ssh, 132 command, 133 check_exit_code=check_exit_code) 134 except Exception as e: 135 LOG.error(e) 136 last_exception = e 137 greenthread.sleep(random.randint(20, 500) / 100.0) 138 try: 139 raise processutils.ProcessExecutionError( 140 exit_code=last_exception.exit_code, 141 stdout=last_exception.stdout, 142 stderr=last_exception.stderr, 143 cmd=last_exception.cmd) 144 except AttributeError: 145 raise processutils.ProcessExecutionError( 146 exit_code=-1, 147 stdout="", 148 stderr="Error running SSH command", 149 cmd=command) 150 151 except Exception: 152 with excutils.save_and_reraise_exception(): 153 LOG.error("Error running SSH command: %s", command) 154 155 def ensure_export(self, context, volume): 156 """Synchronously recreates an export for a logical volume.""" 157 pass 158 159 def create_export(self, context, volume, connector): 160 """Exports the volume.""" 161 pass 162 163 def remove_export(self, context, volume): 164 """Removes an export for a logical volume.""" 165 pass 166 167 def check_for_setup_error(self): 168 """Returns an error if prerequisites aren't met.""" 169 if not self.run_local: 170 if not (self.configuration.san_password or 171 self.configuration.san_private_key): 172 raise exception.InvalidInput( 173 reason=_('Specify san_password or san_private_key')) 174 175 # The san_ip must always be set, because we use it for the target 176 if not self.configuration.san_ip: 177 raise exception.InvalidInput(reason=_("san_ip must be set")) 178 179 180class SanISCSIDriver(SanDriver, driver.ISCSIDriver): 181 def __init__(self, *args, **kwargs): 182 super(SanISCSIDriver, self).__init__(*args, **kwargs) 183 184 def _build_iscsi_target_name(self, volume): 185 return "%s%s" % (self.configuration.target_prefix, 186 volume['name']) 187