Skip to content
Newer
Older
100644 897 lines (694 sloc) 32.3 KB
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
1 # vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3 # Copyright 2011 Justin Santa Barbara
4 # All Rights Reserved.
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License"); you may
7 # not use this file except in compliance with the License. You may obtain
8 # a copy of the License at
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 # License for the specific language governing permissions and limitations
16 # under the License.
17 """
18 Drivers for san-stored volumes.
f86c457 @justinsb PEP 257 fixes
justinsb authored
19
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
20 The unique thing about a SAN is that we don't expect that we can run the volume
f86c457 @justinsb PEP 257 fixes
justinsb authored
21 controller on the SAN hardware. We expect to access it over SSH or some API.
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
22 """
23
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
24 import base64
25 import httplib
26 import json
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
27 import os
28 import paramiko
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
29 import random
30 import socket
31 import string
32 import uuid
f668403 @justinsb Support for HP SAN
justinsb authored
33 from xml.etree import ElementTree
34
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
35 from nova import exception
36 from nova import flags
37 from nova import log as logging
9871c5f @markmc Move cfg to nova.openstack.common
markmc authored
38 from nova.openstack.common import cfg
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
39 from nova import utils
01a938f @YorikSar HACKING fixes, all but sqlalchemy.
YorikSar authored
40 import nova.volume.driver
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
41
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
42
c9ca372 @jkoelker Standardize logging delaration and use
jkoelker authored
43 LOG = logging.getLogger(__name__)
82049af @markmc Refactor away the flags.DEFINE_* helpers
markmc authored
44
45 san_opts = [
46 cfg.BoolOpt('san_thin_provision',
47 default='true',
48 help='Use thin provisioning for SAN volumes?'),
49 cfg.StrOpt('san_ip',
50 default='',
51 help='IP address of SAN controller'),
52 cfg.StrOpt('san_login',
53 default='admin',
54 help='Username for SAN controller'),
55 cfg.StrOpt('san_password',
56 default='',
57 help='Password for SAN controller'),
58 cfg.StrOpt('san_private_key',
59 default='',
60 help='Filename of private key to use for SSH authentication'),
61 cfg.StrOpt('san_clustername',
62 default='',
63 help='Cluster name to use for creating volumes'),
64 cfg.IntOpt('san_ssh_port',
65 default=22,
66 help='SSH port to use with SAN'),
67 cfg.BoolOpt('san_is_local',
68 default='false',
69 help='Execute commands locally instead of over SSH; '
70 'use if the volume service is running on the SAN device'),
71 cfg.StrOpt('san_zfs_volume_base',
72 default='rpool/',
73 help='The ZFS path under which to create zvols for volumes.'),
74 ]
75
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
76 FLAGS = flags.FLAGS
d1888a3 @markmc Remove the last of the gflags shim layer
markmc authored
77 FLAGS.register_opts(san_opts)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
78
d4b4fa9 Fix PEP8 violations
SuperStack authored
79
01a938f @YorikSar HACKING fixes, all but sqlalchemy.
YorikSar authored
80 class SanISCSIDriver(nova.volume.driver.ISCSIDriver):
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
81 """Base class for SAN-style storage volumes
f86c457 @justinsb PEP 257 fixes
justinsb authored
82
83 A SAN-style storage value is 'different' because the volume controller
84 probably won't run on it, so we need to access is over SSH or another
85 remote protocol.
86 """
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
87
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
88 def __init__(self):
89 super(SanISCSIDriver, self).__init__()
90 self.run_local = FLAGS.san_is_local
91
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
92 def _build_iscsi_target_name(self, volume):
93 return "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
94
95 def _connect_to_ssh(self):
96 ssh = paramiko.SSHClient()
97 #TODO(justinsb): We need a better SSH key policy
98 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
99 if FLAGS.san_password:
100 ssh.connect(FLAGS.san_ip,
f668403 @justinsb Support for HP SAN
justinsb authored
101 port=FLAGS.san_ssh_port,
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
102 username=FLAGS.san_login,
103 password=FLAGS.san_password)
ec244a1 @jogo Do not write passwords to verbose logs. bug 916167
jogo authored
104 elif FLAGS.san_private_key:
105 privatekeyfile = os.path.expanduser(FLAGS.san_private_key)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
106 # It sucks that paramiko doesn't support DSA keys
107 privatekey = paramiko.RSAKey.from_private_key_file(privatekeyfile)
d4b4fa9 Fix PEP8 violations
SuperStack authored
108 ssh.connect(FLAGS.san_ip,
f668403 @justinsb Support for HP SAN
justinsb authored
109 port=FLAGS.san_ssh_port,
b1fbb1a Indent args to ssh_connect correctly
SuperStack authored
110 username=FLAGS.san_login,
111 pkey=privatekey)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
112 else:
ec244a1 @jogo Do not write passwords to verbose logs. bug 916167
jogo authored
113 raise exception.Error(_("Specify san_password or san_private_key"))
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
114 return ssh
115
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
116 def _execute(self, *cmd, **kwargs):
117 if self.run_local:
118 return utils.execute(*cmd, **kwargs)
119 else:
120 check_exit_code = kwargs.pop('check_exit_code', None)
121 command = ' '.join(*cmd)
122 return self._run_ssh(command, check_exit_code)
123
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
124 def _run_ssh(self, command, check_exit_code=True):
125 #TODO(justinsb): SSH connection caching (?)
126 ssh = self._connect_to_ssh()
127
128 #TODO(justinsb): Reintroduce the retry hack
01a938f @YorikSar HACKING fixes, all but sqlalchemy.
YorikSar authored
129 ret = utils.ssh_execute(ssh, command, check_exit_code=check_exit_code)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
130
131 ssh.close()
132
133 return ret
134
135 def ensure_export(self, context, volume):
136 """Synchronously recreates an export for a logical volume."""
137 pass
138
139 def create_export(self, context, volume):
140 """Exports the volume."""
141 pass
142
143 def remove_export(self, context, volume):
144 """Removes an export for a logical volume."""
145 pass
146
147 def check_for_setup_error(self):
148 """Returns an error if prerequisites aren't met"""
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
149 if not self.run_local:
ec244a1 @jogo Do not write passwords to verbose logs. bug 916167
jogo authored
150 if not (FLAGS.san_password or FLAGS.san_private_key):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
151 raise exception.Error(_('Specify san_password or '
ec244a1 @jogo Do not write passwords to verbose logs. bug 916167
jogo authored
152 'san_private_key'))
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
153
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
154 # The san_ip must always be set, because we use it for the target
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
155 if not (FLAGS.san_ip):
f668403 @justinsb Support for HP SAN
justinsb authored
156 raise exception.Error(_("san_ip must be set"))
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
157
d4b4fa9 Fix PEP8 violations
SuperStack authored
158
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
159 def _collect_lines(data):
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
160 """Split lines from data into an array, trimming them """
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
161 matches = []
162 for line in data.splitlines():
163 match = line.strip()
164 matches.append(match)
165
166 return matches
167
d4b4fa9 Fix PEP8 violations
SuperStack authored
168
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
169 def _get_prefixed_values(data, prefix):
170 """Collect lines which start with prefix; with trimming"""
171 matches = []
172 for line in data.splitlines():
173 line = line.strip()
174 if line.startswith(prefix):
175 match = line[len(prefix):]
176 match = match.strip()
177 matches.append(match)
178
179 return matches
180
d4b4fa9 Fix PEP8 violations
SuperStack authored
181
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
182 class SolarisISCSIDriver(SanISCSIDriver):
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
183 """Executes commands relating to Solaris-hosted ISCSI volumes.
f86c457 @justinsb PEP 257 fixes
justinsb authored
184
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
185 Basic setup for a Solaris iSCSI server:
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
186
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
187 pkg install storage-server SUNWiscsit
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
188
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
189 svcadm enable stmf
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
190
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
191 svcadm enable -r svc:/network/iscsi/target:default
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
192
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
193 pfexec itadm create-tpg e1000g0 ${MYIP}
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
194
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
195 pfexec itadm create-target -t e1000g0
196
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
197
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
198 Then grant the user that will be logging on lots of permissions.
199 I'm not sure exactly which though:
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
200
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
201 zfs allow justinsb create,mount,destroy rpool
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
202
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
203 usermod -P'File System Management' justinsb
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
204
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
205 usermod -P'Primary Administrator' justinsb
206
ec244a1 @jogo Do not write passwords to verbose logs. bug 916167
jogo authored
207 Also make sure you can login using san_login & san_password/san_private_key
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
208 """
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
209
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
210 def _execute(self, *cmd, **kwargs):
211 new_cmd = ['pfexec']
212 new_cmd.extend(*cmd)
213 return super(SolarisISCSIDriver, self)._execute(self,
214 *new_cmd,
215 **kwargs)
216
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
217 def _view_exists(self, luid):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
218 (out, _err) = self._execute('/usr/sbin/stmfadm',
219 'list-view', '-l', luid,
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
220 check_exit_code=False)
221 if "no views found" in out:
222 return False
223
224 if "View Entry:" in out:
225 return True
226
227 raise exception.Error("Cannot parse list-view output: %s" % (out))
228
229 def _get_target_groups(self):
230 """Gets list of target groups from host."""
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
231 (out, _err) = self._execute('/usr/sbin/stmfadm', 'list-tg')
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
232 matches = _get_prefixed_values(out, 'Target group: ')
233 LOG.debug("target_groups=%s" % matches)
234 return matches
235
236 def _target_group_exists(self, target_group_name):
237 return target_group_name not in self._get_target_groups()
238
239 def _get_target_group_members(self, target_group_name):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
240 (out, _err) = self._execute('/usr/sbin/stmfadm',
241 'list-tg', '-v', target_group_name)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
242 matches = _get_prefixed_values(out, 'Member: ')
243 LOG.debug("members of %s=%s" % (target_group_name, matches))
244 return matches
245
246 def _is_target_group_member(self, target_group_name, iscsi_target_name):
247 return iscsi_target_name in (
248 self._get_target_group_members(target_group_name))
249
250 def _get_iscsi_targets(self):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
251 (out, _err) = self._execute('/usr/sbin/itadm', 'list-target')
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
252 matches = _collect_lines(out)
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
253
254 # Skip header
255 if len(matches) != 0:
256 assert 'TARGET NAME' in matches[0]
257 matches = matches[1:]
258
259 targets = []
260 for line in matches:
261 items = line.split()
262 assert len(items) == 3
263 targets.append(items[0])
264
265 LOG.debug("_get_iscsi_targets=%s" % (targets))
266 return targets
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
267
268 def _iscsi_target_exists(self, iscsi_target_name):
269 return iscsi_target_name in self._get_iscsi_targets()
270
271 def _build_zfs_poolname(self, volume):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
272 zfs_poolname = '%s%s' % (FLAGS.san_zfs_volume_base, volume['name'])
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
273 return zfs_poolname
274
275 def create_volume(self, volume):
276 """Creates a volume."""
277 if int(volume['size']) == 0:
278 sizestr = '100M'
279 else:
280 sizestr = '%sG' % volume['size']
281
282 zfs_poolname = self._build_zfs_poolname(volume)
283
284 # Create a zfs volume
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
285 cmd = ['/usr/sbin/zfs', 'create']
286 if FLAGS.san_thin_provision:
287 cmd.append('-s')
288 cmd.extend(['-V', sizestr])
289 cmd.append(zfs_poolname)
290 self._execute(*cmd)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
291
292 def _get_luid(self, volume):
293 zfs_poolname = self._build_zfs_poolname(volume)
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
294 zvol_name = '/dev/zvol/rdsk/%s' % zfs_poolname
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
295
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
296 (out, _err) = self._execute('/usr/sbin/sbdadm', 'list-lu')
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
297
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
298 lines = _collect_lines(out)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
299
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
300 # Strip headers
301 if len(lines) >= 1:
302 if lines[0] == '':
303 lines = lines[1:]
304
305 if len(lines) >= 4:
306 assert 'Found' in lines[0]
307 assert '' == lines[1]
308 assert 'GUID' in lines[2]
309 assert '------------------' in lines[3]
310
311 lines = lines[4:]
312
313 for line in lines:
314 items = line.split()
315 assert len(items) == 3
316 if items[2] == zvol_name:
317 luid = items[0].strip()
318 return luid
319
320 raise Exception(_('LUID not found for %(zfs_poolname)s. '
321 'Output=%(out)s') % locals())
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
322
323 def _is_lu_created(self, volume):
324 luid = self._get_luid(volume)
325 return luid
326
327 def delete_volume(self, volume):
328 """Deletes a volume."""
329 zfs_poolname = self._build_zfs_poolname(volume)
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
330 self._execute('/usr/sbin/zfs', 'destroy', zfs_poolname)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
331
332 def local_path(self, volume):
333 # TODO(justinsb): Is this needed here?
334 escaped_group = FLAGS.volume_group.replace('-', '--')
335 escaped_name = volume['name'].replace('-', '--')
336 return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)
337
338 def ensure_export(self, context, volume):
339 """Synchronously recreates an export for a logical volume."""
340 #TODO(justinsb): On bootup, this is called for every volume.
341 # It then runs ~5 SSH commands for each volume,
342 # most of which fetch the same info each time
343 # This makes initial start stupid-slow
1314ee0 @justinsb create_export and ensure_export should pass up the return value, to u…
justinsb authored
344 return self._do_export(volume, force_create=False)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
345
346 def create_export(self, context, volume):
1314ee0 @justinsb create_export and ensure_export should pass up the return value, to u…
justinsb authored
347 return self._do_export(volume, force_create=True)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
348
349 def _do_export(self, volume, force_create):
350 # Create a Logical Unit (LU) backed by the zfs volume
351 zfs_poolname = self._build_zfs_poolname(volume)
352
353 if force_create or not self._is_lu_created(volume):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
354 zvol_name = '/dev/zvol/rdsk/%s' % zfs_poolname
355 self._execute('/usr/sbin/sbdadm', 'create-lu', zvol_name)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
356
357 luid = self._get_luid(volume)
358 iscsi_name = self._build_iscsi_target_name(volume)
359 target_group_name = 'tg-%s' % volume['name']
360
361 # Create a iSCSI target, mapped to just this volume
362 if force_create or not self._target_group_exists(target_group_name):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
363 self._execute('/usr/sbin/stmfadm', 'create-tg', target_group_name)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
364
365 # Yes, we add the initiatior before we create it!
5315d50 @justinsb Fixes for Vish & Devin's feedback
justinsb authored
366 # Otherwise, it complains that the target is already active
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
367 if force_create or not self._is_target_group_member(target_group_name,
368 iscsi_name):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
369 self._execute('/usr/sbin/stmfadm',
370 'add-tg-member', '-g', target_group_name, iscsi_name)
371
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
372 if force_create or not self._iscsi_target_exists(iscsi_name):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
373 self._execute('/usr/sbin/itadm', 'create-target', '-n', iscsi_name)
374
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
375 if force_create or not self._view_exists(luid):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
376 self._execute('/usr/sbin/stmfadm',
377 'add-view', '-t', target_group_name, luid)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
378
f668403 @justinsb Support for HP SAN
justinsb authored
379 #TODO(justinsb): Is this always 1? Does it matter?
380 iscsi_portal_interface = '1'
381 iscsi_portal = FLAGS.san_ip + ":3260," + iscsi_portal_interface
382
383 db_update = {}
384 db_update['provider_location'] = ("%s %s" %
385 (iscsi_portal,
386 iscsi_name))
387
388 return db_update
389
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
390 def remove_export(self, context, volume):
391 """Removes an export for a logical volume."""
392
393 # This is the reverse of _do_export
394 luid = self._get_luid(volume)
395 iscsi_name = self._build_iscsi_target_name(volume)
396 target_group_name = 'tg-%s' % volume['name']
397
398 if self._view_exists(luid):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
399 self._execute('/usr/sbin/stmfadm', 'remove-view', '-l', luid, '-a')
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
400
401 if self._iscsi_target_exists(iscsi_name):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
402 self._execute('/usr/sbin/stmfadm', 'offline-target', iscsi_name)
403 self._execute('/usr/sbin/itadm', 'delete-target', iscsi_name)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
404
405 # We don't delete the tg-member; we delete the whole tg!
406
407 if self._target_group_exists(target_group_name):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
408 self._execute('/usr/sbin/stmfadm', 'delete-tg', target_group_name)
62d2a1a @justinsb Implementation of 'SAN' volumes
justinsb authored
409
410 if self._is_lu_created(volume):
1f364d3 @justinsb Support local target for Solaris, use 'safe' command-line processing
justinsb authored
411 self._execute('/usr/sbin/sbdadm', 'delete-lu', luid)
f668403 @justinsb Support for HP SAN
justinsb authored
412
413
414 class HpSanISCSIDriver(SanISCSIDriver):
415 """Executes commands relating to HP/Lefthand SAN ISCSI volumes.
f86c457 @justinsb PEP 257 fixes
justinsb authored
416
f668403 @justinsb Support for HP SAN
justinsb authored
417 We use the CLIQ interface, over SSH.
418
419 Rough overview of CLIQ commands used:
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
420
f86c457 @justinsb PEP 257 fixes
justinsb authored
421 :createVolume: (creates the volume)
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
422
f86c457 @justinsb PEP 257 fixes
justinsb authored
423 :getVolumeInfo: (to discover the IQN etc)
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
424
f86c457 @justinsb PEP 257 fixes
justinsb authored
425 :getClusterInfo: (to discover the iSCSI target IP address)
0a64949 @justinsb Documentation fixes so that output looks better
justinsb authored
426
f86c457 @justinsb PEP 257 fixes
justinsb authored
427 :assignVolumeChap: (exports it with CHAP security)
f668403 @justinsb Support for HP SAN
justinsb authored
428
429 The 'trick' here is that the HP SAN enforces security by default, so
430 normally a volume mount would need both to configure the SAN in the volume
431 layer and do the mount on the compute layer. Multi-layer operations are
432 not catered for at the moment in the nova architecture, so instead we
433 share the volume using CHAP at volume creation time. Then the mount need
434 only use those CHAP credentials, so can take place exclusively in the
f86c457 @justinsb PEP 257 fixes
justinsb authored
435 compute layer.
436 """
f668403 @justinsb Support for HP SAN
justinsb authored
437
438 def _cliq_run(self, verb, cliq_args):
439 """Runs a CLIQ command over SSH, without doing any result parsing"""
440 cliq_arg_strings = []
441 for k, v in cliq_args.items():
442 cliq_arg_strings.append(" %s=%s" % (k, v))
443 cmd = verb + ''.join(cliq_arg_strings)
444
445 return self._run_ssh(cmd)
446
447 def _cliq_run_xml(self, verb, cliq_args, check_cliq_result=True):
448 """Runs a CLIQ command over SSH, parsing and checking the output"""
449 cliq_args['output'] = 'XML'
450 (out, _err) = self._cliq_run(verb, cliq_args)
451
452 LOG.debug(_("CLIQ command returned %s"), out)
453
454 result_xml = ElementTree.fromstring(out)
455 if check_cliq_result:
456 response_node = result_xml.find("response")
457 if response_node is None:
458 msg = (_("Malformed response to CLIQ command "
459 "%(verb)s %(cliq_args)s. Result=%(out)s") %
460 locals())
461 raise exception.Error(msg)
462
463 result_code = response_node.attrib.get("result")
464
465 if result_code != "0":
466 msg = (_("Error running CLIQ command %(verb)s %(cliq_args)s. "
467 " Result=%(out)s") %
468 locals())
469 raise exception.Error(msg)
470
471 return result_xml
472
473 def _cliq_get_cluster_info(self, cluster_name):
474 """Queries for info about the cluster (including IP)"""
475 cliq_args = {}
476 cliq_args['clusterName'] = cluster_name
477 cliq_args['searchDepth'] = '1'
478 cliq_args['verbose'] = '0'
479
480 result_xml = self._cliq_run_xml("getClusterInfo", cliq_args)
481
482 return result_xml
483
484 def _cliq_get_cluster_vip(self, cluster_name):
485 """Gets the IP on which a cluster shares iSCSI volumes"""
486 cluster_xml = self._cliq_get_cluster_info(cluster_name)
487
488 vips = []
489 for vip in cluster_xml.findall("response/cluster/vip"):
490 vips.append(vip.attrib.get('ipAddress'))
491
492 if len(vips) == 1:
493 return vips[0]
494
495 _xml = ElementTree.tostring(cluster_xml)
496 msg = (_("Unexpected number of virtual ips for cluster "
497 " %(cluster_name)s. Result=%(_xml)s") %
498 locals())
499 raise exception.Error(msg)
500
501 def _cliq_get_volume_info(self, volume_name):
502 """Gets the volume info, including IQN"""
503 cliq_args = {}
504 cliq_args['volumeName'] = volume_name
505 result_xml = self._cliq_run_xml("getVolumeInfo", cliq_args)
506
507 # Result looks like this:
508 #<gauche version="1.0">
509 # <response description="Operation succeeded." name="CliqSuccess"
510 # processingTime="87" result="0">
511 # <volume autogrowPages="4" availability="online" blockSize="1024"
512 # bytesWritten="0" checkSum="false" clusterName="Cluster01"
513 # created="2011-02-08T19:56:53Z" deleting="false" description=""
514 # groupName="Group01" initialQuota="536870912" isPrimary="true"
515 # iscsiIqn="iqn.2003-10.com.lefthandnetworks:group01:25366:vol-b"
516 # maxSize="6865387257856" md5="9fa5c8b2cca54b2948a63d833097e1ca"
517 # minReplication="1" name="vol-b" parity="0" replication="2"
518 # reserveQuota="536870912" scratchQuota="4194304"
519 # serialNumber="9fa5c8b2cca54b2948a63d833097e1ca0000000000006316"
520 # size="1073741824" stridePages="32" thinProvision="true">
521 # <status description="OK" value="2"/>
522 # <permission access="rw"
523 # authGroup="api-34281B815713B78-(trimmed)51ADD4B7030853AA7"
524 # chapName="chapusername" chapRequired="true" id="25369"
525 # initiatorSecret="" iqn="" iscsiEnabled="true"
526 # loadBalance="true" targetSecret="supersecret"/>
527 # </volume>
528 # </response>
529 #</gauche>
530
531 # Flatten the nodes into a dictionary; use prefixes to avoid collisions
532 volume_attributes = {}
533
534 volume_node = result_xml.find("response/volume")
535 for k, v in volume_node.attrib.items():
536 volume_attributes["volume." + k] = v
537
538 status_node = volume_node.find("status")
539 if not status_node is None:
540 for k, v in status_node.attrib.items():
541 volume_attributes["status." + k] = v
542
543 # We only consider the first permission node
544 permission_node = volume_node.find("permission")
545 if not permission_node is None:
546 for k, v in status_node.attrib.items():
547 volume_attributes["permission." + k] = v
548
549 LOG.debug(_("Volume info: %(volume_name)s => %(volume_attributes)s") %
550 locals())
551 return volume_attributes
552
553 def create_volume(self, volume):
554 """Creates a volume."""
555 cliq_args = {}
556 cliq_args['clusterName'] = FLAGS.san_clustername
557 #TODO(justinsb): Should we default to inheriting thinProvision?
558 cliq_args['thinProvision'] = '1' if FLAGS.san_thin_provision else '0'
559 cliq_args['volumeName'] = volume['name']
560 if int(volume['size']) == 0:
561 cliq_args['size'] = '100MB'
562 else:
563 cliq_args['size'] = '%sGB' % volume['size']
564
565 self._cliq_run_xml("createVolume", cliq_args)
566
567 volume_info = self._cliq_get_volume_info(volume['name'])
568 cluster_name = volume_info['volume.clusterName']
569 iscsi_iqn = volume_info['volume.iscsiIqn']
570
571 #TODO(justinsb): Is this always 1? Does it matter?
572 cluster_interface = '1'
573
574 cluster_vip = self._cliq_get_cluster_vip(cluster_name)
575 iscsi_portal = cluster_vip + ":3260," + cluster_interface
576
f797d6c @justinsb Renamed db_update to model_update, and lots more documentation
justinsb authored
577 model_update = {}
578 model_update['provider_location'] = ("%s %s" %
579 (iscsi_portal,
580 iscsi_iqn))
f668403 @justinsb Support for HP SAN
justinsb authored
581
f797d6c @justinsb Renamed db_update to model_update, and lots more documentation
justinsb authored
582 return model_update
f668403 @justinsb Support for HP SAN
justinsb authored
583
584 def delete_volume(self, volume):
585 """Deletes a volume."""
586 cliq_args = {}
587 cliq_args['volumeName'] = volume['name']
588 cliq_args['prompt'] = 'false' # Don't confirm
589
590 self._cliq_run_xml("deleteVolume", cliq_args)
591
592 def local_path(self, volume):
593 # TODO(justinsb): Is this needed here?
594 raise exception.Error(_("local_path not supported"))
595
596 def ensure_export(self, context, volume):
597 """Synchronously recreates an export for a logical volume."""
598 return self._do_export(context, volume, force_create=False)
599
600 def create_export(self, context, volume):
601 return self._do_export(context, volume, force_create=True)
602
603 def _do_export(self, context, volume, force_create):
604 """Supports ensure_export and create_export"""
605 volume_info = self._cliq_get_volume_info(volume['name'])
606
607 is_shared = 'permission.authGroup' in volume_info
608
f797d6c @justinsb Renamed db_update to model_update, and lots more documentation
justinsb authored
609 model_update = {}
f668403 @justinsb Support for HP SAN
justinsb authored
610
611 should_export = False
612
613 if force_create or not is_shared:
614 should_export = True
615 # Check that we have a project_id
616 project_id = volume['project_id']
617 if not project_id:
618 project_id = context.project_id
619
620 if project_id:
621 #TODO(justinsb): Use a real per-project password here
622 chap_username = 'proj_' + project_id
623 # HP/Lefthand requires that the password be >= 12 characters
624 chap_password = 'project_secret_' + project_id
625 else:
626 msg = (_("Could not determine project for volume %s, "
627 "can't export") %
628 (volume['name']))
629 if force_create:
630 raise exception.Error(msg)
631 else:
632 LOG.warn(msg)
633 should_export = False
634
635 if should_export:
636 cliq_args = {}
637 cliq_args['volumeName'] = volume['name']
638 cliq_args['chapName'] = chap_username
639 cliq_args['targetSecret'] = chap_password
640
641 self._cliq_run_xml("assignVolumeChap", cliq_args)
642
f797d6c @justinsb Renamed db_update to model_update, and lots more documentation
justinsb authored
643 model_update['provider_auth'] = ("CHAP %s %s" %
644 (chap_username, chap_password))
f668403 @justinsb Support for HP SAN
justinsb authored
645
f797d6c @justinsb Renamed db_update to model_update, and lots more documentation
justinsb authored
646 return model_update
f668403 @justinsb Support for HP SAN
justinsb authored
647
648 def remove_export(self, context, volume):
649 """Removes an export for a logical volume."""
650 cliq_args = {}
651 cliq_args['volumeName'] = volume['name']
652
653 self._cliq_run_xml("unassignVolume", cliq_args)
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
654
655
656 class SolidFireSanISCSIDriver(SanISCSIDriver):
657
658 def _issue_api_request(self, method_name, params):
659 """All API requests to SolidFire device go through this method
660
661 Simple json-rpc web based API calls.
662 each call takes a set of paramaters (dict)
663 and returns results in a dict as well.
664 """
665
666 host = FLAGS.san_ip
667 # For now 443 is the only port our server accepts requests on
668 port = 443
669
670 # NOTE(john-griffith): Probably don't need this, but the idea is
671 # we provide a request_id so we can correlate
672 # responses with requests
673 request_id = int(uuid.uuid4()) # just generate a random number
674
675 cluster_admin = FLAGS.san_login
676 cluster_password = FLAGS.san_password
677
678 command = {'method': method_name,
679 'id': request_id}
680
681 if params is not None:
682 command['params'] = params
683
684 payload = json.dumps(command, ensure_ascii=False)
685 payload.encode('utf-8')
686 # we use json-rpc, webserver needs to see json-rpc in header
687 header = {'Content-Type': 'application/json-rpc; charset=utf-8'}
688
689 if cluster_password is not None:
690 # base64.encodestring includes a newline character
691 # in the result, make sure we strip it off
692 auth_key = base64.encodestring('%s:%s' % (cluster_admin,
693 cluster_password))[:-1]
694 header['Authorization'] = 'Basic %s' % auth_key
695
534a894 @jerdfelt Only raw string literals should be used with _()
jerdfelt authored
696 LOG.debug(_("Payload for SolidFire API call: %s") % payload)
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
697 connection = httplib.HTTPSConnection(host, port)
698 connection.request('POST', '/json-rpc/1.0', payload, header)
699 response = connection.getresponse()
700 data = {}
701
702 if response.status != 200:
703 connection.close()
afd5b22 @bcwaldon Replace ApiError with new exceptions
bcwaldon authored
704 raise exception.SolidFireAPIException(status=response.status)
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
705
706 else:
707 data = response.read()
708 try:
709 data = json.loads(data)
710
711 except (TypeError, ValueError), exc:
712 connection.close()
534a894 @jerdfelt Only raw string literals should be used with _()
jerdfelt authored
713 msg = _("Call to json.loads() raised an exception: %s") % exc
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
714 raise exception.SfJsonEncodeFailure(msg)
715
716 connection.close()
717
534a894 @jerdfelt Only raw string literals should be used with _()
jerdfelt authored
718 LOG.debug(_("Results of SolidFire API call: %s") % data)
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
719 return data
720
721 def _get_volumes_by_sfaccount(self, account_id):
722 params = {'accountID': account_id}
723 data = self._issue_api_request('ListVolumesForAccount', params)
724 if 'result' in data:
725 return data['result']['volumes']
726
727 def _get_sfaccount_by_name(self, sf_account_name):
728 sfaccount = None
729 params = {'username': sf_account_name}
730 data = self._issue_api_request('GetAccountByName', params)
731 if 'result' in data and 'account' in data['result']:
534a894 @jerdfelt Only raw string literals should be used with _()
jerdfelt authored
732 LOG.debug(_('Found solidfire account: %s') % sf_account_name)
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
733 sfaccount = data['result']['account']
734 return sfaccount
735
736 def _create_sfaccount(self, nova_project_id):
737 """Create account on SolidFire device if it doesn't already exist.
738
739 We're first going to check if the account already exits, if it does
740 just return it. If not, then create it.
741 """
742
743 sf_account_name = socket.gethostname() + '-' + nova_project_id
744 sfaccount = self._get_sfaccount_by_name(sf_account_name)
745 if sfaccount is None:
534a894 @jerdfelt Only raw string literals should be used with _()
jerdfelt authored
746 LOG.debug(_('solidfire account: %s does not exist, create it...')
747 % sf_account_name)
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
748 chap_secret = self._generate_random_string(12)
749 params = {'username': sf_account_name,
750 'initiatorSecret': chap_secret,
751 'targetSecret': chap_secret,
752 'attributes': {}}
753 data = self._issue_api_request('AddAccount', params)
754 if 'result' in data:
755 sfaccount = self._get_sfaccount_by_name(sf_account_name)
756
757 return sfaccount
758
759 def _get_cluster_info(self):
760 params = {}
761 data = self._issue_api_request('GetClusterInfo', params)
762 if 'result' not in data:
afd5b22 @bcwaldon Replace ApiError with new exceptions
bcwaldon authored
763 raise exception.SolidFireAPIDataException(data=data)
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
764
765 return data['result']
766
767 def _do_export(self, volume):
768 """Gets the associated account, retrieves CHAP info and updates."""
769
770 sfaccount_name = '%s-%s' % (socket.gethostname(), volume['project_id'])
771 sfaccount = self._get_sfaccount_by_name(sfaccount_name)
772
773 model_update = {}
774 model_update['provider_auth'] = ('CHAP %s %s'
775 % (sfaccount['username'], sfaccount['targetSecret']))
776
777 return model_update
778
779 def _generate_random_string(self, length):
780 """Generates random_string to use for CHAP password."""
781
782 char_set = string.ascii_uppercase + string.digits
783 return ''.join(random.sample(char_set, length))
784
785 def create_volume(self, volume):
786 """Create volume on SolidFire device.
787
788 The account is where CHAP settings are derived from, volume is
789 created and exported. Note that the new volume is immediately ready
790 for use.
791
792 One caveat here is that an existing user account must be specified
793 in the API call to create a new volume. We use a set algorithm to
794 determine account info based on passed in nova volume object. First
795 we check to see if the account already exists (and use it), or if it
796 does not already exist, we'll go ahead and create it.
797
798 For now, we're just using very basic settings, QOS is
799 turned off, 512 byte emulation is off etc. Will be
800 looking at extensions for these things later, or
801 this module can be hacked to suit needs.
802 """
803
804 LOG.debug(_("Enter SolidFire create_volume..."))
805 GB = 1048576 * 1024
806 slice_count = 1
807 enable_emulation = False
808 attributes = {}
809
810 cluster_info = self._get_cluster_info()
811 iscsi_portal = cluster_info['clusterInfo']['svip'] + ':3260'
812 sfaccount = self._create_sfaccount(volume['project_id'])
813 account_id = sfaccount['accountID']
814 account_name = sfaccount['username']
815 chap_secret = sfaccount['targetSecret']
816
817 params = {'name': volume['name'],
818 'accountID': account_id,
819 'sliceCount': slice_count,
820 'totalSize': volume['size'] * GB,
821 'enable512e': enable_emulation,
822 'attributes': attributes}
823
824 data = self._issue_api_request('CreateVolume', params)
afd5b22 @bcwaldon Replace ApiError with new exceptions
bcwaldon authored
825
826 if 'result' not in data or 'volumeID' not in data['result']:
827 raise exception.SolidFireAPIDataException(data=data)
828
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
829 volume_id = data['result']['volumeID']
830
831 volume_list = self._get_volumes_by_sfaccount(account_id)
832 iqn = None
833 for v in volume_list:
834 if v['volumeID'] == volume_id:
835 iqn = 'iqn.2010-01.com.solidfire:' + v['iqn']
836 break
837
838 model_update = {}
0c483d1 @j-griffith Add lun number to provider_location in create_volume
j-griffith authored
839
840 # NOTE(john-griffith): SF volumes are always at lun 0
841 model_update['provider_location'] = ('%s %s %s'
842 % (iscsi_portal, iqn, 0))
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
843 model_update['provider_auth'] = ('CHAP %s %s'
844 % (account_name, chap_secret))
845
846 LOG.debug(_("Leaving SolidFire create_volume"))
847 return model_update
848
849 def delete_volume(self, volume):
850 """Delete SolidFire Volume from device.
851
852 SolidFire allows multipe volumes with same name,
853 volumeID is what's guaranteed unique.
854
855 What we'll do here is check volumes based on account. this
856 should work because nova will increment it's volume_id
857 so we should always get the correct volume. This assumes
858 that nova does not assign duplicate ID's.
859 """
860
861 LOG.debug(_("Enter SolidFire delete_volume..."))
862 sf_account_name = socket.gethostname() + '-' + volume['project_id']
863 sfaccount = self._get_sfaccount_by_name(sf_account_name)
864 if sfaccount is None:
865 raise exception.SfAccountNotFound(account_name=sf_account_name)
866
867 params = {'accountID': sfaccount['accountID']}
868 data = self._issue_api_request('ListVolumesForAccount', params)
869 if 'result' not in data:
afd5b22 @bcwaldon Replace ApiError with new exceptions
bcwaldon authored
870 raise exception.SolidFireAPIDataException(data=data)
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
871
872 found_count = 0
873 volid = -1
874 for v in data['result']['volumes']:
875 if v['name'] == volume['name']:
876 found_count += 1
877 volid = v['volumeID']
878
879 if found_count != 1:
534a894 @jerdfelt Only raw string literals should be used with _()
jerdfelt authored
880 LOG.debug(_("Deleting volumeID: %s ") % volid)
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
881 raise exception.DuplicateSfVolumeNames(vol_name=volume['name'])
882
883 params = {'volumeID': volid}
884 data = self._issue_api_request('DeleteVolume', params)
885 if 'result' not in data:
afd5b22 @bcwaldon Replace ApiError with new exceptions
bcwaldon authored
886 raise exception.SolidFireAPIDataException(data=data)
c9ac6e1 @j-griffith Implementation of new Nova Volume driver for SolidFire ISCSI SAN
j-griffith authored
887
888 LOG.debug(_("Leaving SolidFire delete_volume"))
889
890 def ensure_export(self, context, volume):
891 LOG.debug(_("Executing SolidFire ensure_export..."))
892 return self._do_export(volume)
893
894 def create_export(self, context, volume):
895 LOG.debug(_("Executing SolidFire create_export..."))
896 return self._do_export(volume)
Something went wrong with that request. Please try again.