~zulcss/cinder/cinder-ca-g2

« back to all changes in this revision

Viewing changes to cinder/volume/san.py

  • Committer: Package Import Robot
  • Author(s): Adam Gandelman, Adam Gandelman, James Page
  • Date: 2012-11-01 11:30:15 UTC
  • mfrom: (1.1.8)
  • Revision ID: package-import@ubuntu.com-20121101113015-405w8kcosepo601w
Tags: 2013.1~g1~20121101.361-0ubuntu1
[ Adam Gandelman ]
* New upstream release.
* debian/patches/0001-Replace-builtin-hash.patch: Dropped.

[ James Page ]
* Cinder should suggest ceph-common, not python-ceph (LP: #1065901):
  - debian/control: cinder-volume Suggests: python-ceph -> ceph-common

Show diffs side-by-side

added added

removed removed

Lines of Context:
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.
19
 
 
20
 
The unique thing about a SAN is that we don't expect that we can run the volume
21
 
controller on the SAN hardware.  We expect to access it over SSH or some API.
22
 
"""
23
 
 
24
 
import base64
25
 
import httplib
26
 
import os
27
 
import paramiko
28
 
import random
29
 
import socket
30
 
import string
31
 
import uuid
32
 
 
33
 
from lxml import etree
34
 
 
35
 
from cinder import exception
36
 
from cinder import flags
37
 
from cinder.openstack.common import log as logging
38
 
from cinder.openstack.common import cfg
39
 
from cinder.openstack.common import jsonutils
40
 
from cinder import utils
41
 
import cinder.volume.driver
42
 
 
43
 
 
44
 
LOG = logging.getLogger(__name__)
45
 
 
46
 
san_opts = [
47
 
    cfg.BoolOpt('san_thin_provision',
48
 
                default=True,
49
 
                help='Use thin provisioning for SAN volumes?'),
50
 
    cfg.StrOpt('san_ip',
51
 
               default='',
52
 
               help='IP address of SAN controller'),
53
 
    cfg.StrOpt('san_login',
54
 
               default='admin',
55
 
               help='Username for SAN controller'),
56
 
    cfg.StrOpt('san_password',
57
 
               default='',
58
 
               help='Password for SAN controller'),
59
 
    cfg.StrOpt('san_private_key',
60
 
               default='',
61
 
               help='Filename of private key to use for SSH authentication'),
62
 
    cfg.StrOpt('san_clustername',
63
 
               default='',
64
 
               help='Cluster name to use for creating volumes'),
65
 
    cfg.IntOpt('san_ssh_port',
66
 
               default=22,
67
 
               help='SSH port to use with SAN'),
68
 
    cfg.BoolOpt('san_is_local',
69
 
                default=False,
70
 
                help='Execute commands locally instead of over SSH; '
71
 
                     'use if the volume service is running on the SAN device'),
72
 
    cfg.StrOpt('san_zfs_volume_base',
73
 
               default='rpool/',
74
 
               help='The ZFS path under which to create zvols for volumes.'),
75
 
]
76
 
 
77
 
FLAGS = flags.FLAGS
78
 
FLAGS.register_opts(san_opts)
79
 
 
80
 
 
81
 
class SanISCSIDriver(cinder.volume.driver.ISCSIDriver):
82
 
    """Base class for SAN-style storage volumes
83
 
 
84
 
    A SAN-style storage value is 'different' because the volume controller
85
 
    probably won't run on it, so we need to access is over SSH or another
86
 
    remote protocol.
87
 
    """
88
 
 
89
 
    def __init__(self, *args, **kwargs):
90
 
        super(SanISCSIDriver, self).__init__(*args, **kwargs)
91
 
        self.run_local = FLAGS.san_is_local
92
 
 
93
 
    def _build_iscsi_target_name(self, volume):
94
 
        return "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
95
 
 
96
 
    def _connect_to_ssh(self):
97
 
        ssh = paramiko.SSHClient()
98
 
        #TODO(justinsb): We need a better SSH key policy
99
 
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
100
 
        if FLAGS.san_password:
101
 
            ssh.connect(FLAGS.san_ip,
102
 
                        port=FLAGS.san_ssh_port,
103
 
                        username=FLAGS.san_login,
104
 
                        password=FLAGS.san_password)
105
 
        elif FLAGS.san_private_key:
106
 
            privatekeyfile = os.path.expanduser(FLAGS.san_private_key)
107
 
            # It sucks that paramiko doesn't support DSA keys
108
 
            privatekey = paramiko.RSAKey.from_private_key_file(privatekeyfile)
109
 
            ssh.connect(FLAGS.san_ip,
110
 
                        port=FLAGS.san_ssh_port,
111
 
                        username=FLAGS.san_login,
112
 
                        pkey=privatekey)
113
 
        else:
114
 
            msg = _("Specify san_password or san_private_key")
115
 
            raise exception.InvalidInput(reason=msg)
116
 
        return ssh
117
 
 
118
 
    def _execute(self, *cmd, **kwargs):
119
 
        if self.run_local:
120
 
            return utils.execute(*cmd, **kwargs)
121
 
        else:
122
 
            check_exit_code = kwargs.pop('check_exit_code', None)
123
 
            command = ' '.join(cmd)
124
 
            return self._run_ssh(command, check_exit_code)
125
 
 
126
 
    def _run_ssh(self, command, check_exit_code=True):
127
 
        #TODO(justinsb): SSH connection caching (?)
128
 
        ssh = self._connect_to_ssh()
129
 
 
130
 
        #TODO(justinsb): Reintroduce the retry hack
131
 
        ret = utils.ssh_execute(ssh, command, check_exit_code=check_exit_code)
132
 
 
133
 
        ssh.close()
134
 
 
135
 
        return ret
136
 
 
137
 
    def ensure_export(self, context, volume):
138
 
        """Synchronously recreates an export for a logical volume."""
139
 
        pass
140
 
 
141
 
    def create_export(self, context, volume):
142
 
        """Exports the volume."""
143
 
        pass
144
 
 
145
 
    def remove_export(self, context, volume):
146
 
        """Removes an export for a logical volume."""
147
 
        pass
148
 
 
149
 
    def check_for_setup_error(self):
150
 
        """Returns an error if prerequisites aren't met."""
151
 
        if not self.run_local:
152
 
            if not (FLAGS.san_password or FLAGS.san_private_key):
153
 
                raise exception.InvalidInput(
154
 
                    reason=_('Specify san_password or san_private_key'))
155
 
 
156
 
        # The san_ip must always be set, because we use it for the target
157
 
        if not (FLAGS.san_ip):
158
 
            raise exception.InvalidInput(reason=_("san_ip must be set"))
159
 
 
160
 
 
161
 
def _collect_lines(data):
162
 
    """Split lines from data into an array, trimming them """
163
 
    matches = []
164
 
    for line in data.splitlines():
165
 
        match = line.strip()
166
 
        matches.append(match)
167
 
 
168
 
    return matches
169
 
 
170
 
 
171
 
def _get_prefixed_values(data, prefix):
172
 
    """Collect lines which start with prefix; with trimming"""
173
 
    matches = []
174
 
    for line in data.splitlines():
175
 
        line = line.strip()
176
 
        if line.startswith(prefix):
177
 
            match = line[len(prefix):]
178
 
            match = match.strip()
179
 
            matches.append(match)
180
 
 
181
 
    return matches
182
 
 
183
 
 
184
 
class SolarisISCSIDriver(SanISCSIDriver):
185
 
    """Executes commands relating to Solaris-hosted ISCSI volumes.
186
 
 
187
 
    Basic setup for a Solaris iSCSI server:
188
 
 
189
 
    pkg install storage-server SUNWiscsit
190
 
 
191
 
    svcadm enable stmf
192
 
 
193
 
    svcadm enable -r svc:/network/iscsi/target:default
194
 
 
195
 
    pfexec itadm create-tpg e1000g0 ${MYIP}
196
 
 
197
 
    pfexec itadm create-target -t e1000g0
198
 
 
199
 
 
200
 
    Then grant the user that will be logging on lots of permissions.
201
 
    I'm not sure exactly which though:
202
 
 
203
 
    zfs allow justinsb create,mount,destroy rpool
204
 
 
205
 
    usermod -P'File System Management' justinsb
206
 
 
207
 
    usermod -P'Primary Administrator' justinsb
208
 
 
209
 
    Also make sure you can login using san_login & san_password/san_private_key
210
 
    """
211
 
    def __init__(self, *cmd, **kwargs):
212
 
        super(SolarisISCSIDriver, self).__init__(*cmd,
213
 
                                                 execute=self._execute,
214
 
                                                 **kwargs)
215
 
 
216
 
    def _execute(self, *cmd, **kwargs):
217
 
        new_cmd = ['pfexec']
218
 
        new_cmd.extend(cmd)
219
 
        return super(SolarisISCSIDriver, self)._execute(*new_cmd,
220
 
                                                        **kwargs)
221
 
 
222
 
    def _view_exists(self, luid):
223
 
        (out, _err) = self._execute('/usr/sbin/stmfadm',
224
 
                                    'list-view', '-l', luid,
225
 
                                    check_exit_code=False)
226
 
        if "no views found" in out:
227
 
            return False
228
 
 
229
 
        if "View Entry:" in out:
230
 
            return True
231
 
        msg = _("Cannot parse list-view output: %s") % out
232
 
        raise exception.VolumeBackendAPIException(data=msg)
233
 
 
234
 
    def _get_target_groups(self):
235
 
        """Gets list of target groups from host."""
236
 
        (out, _err) = self._execute('/usr/sbin/stmfadm', 'list-tg')
237
 
        matches = _get_prefixed_values(out, 'Target group: ')
238
 
        LOG.debug("target_groups=%s" % matches)
239
 
        return matches
240
 
 
241
 
    def _target_group_exists(self, target_group_name):
242
 
        return target_group_name not in self._get_target_groups()
243
 
 
244
 
    def _get_target_group_members(self, target_group_name):
245
 
        (out, _err) = self._execute('/usr/sbin/stmfadm',
246
 
                                    'list-tg', '-v', target_group_name)
247
 
        matches = _get_prefixed_values(out, 'Member: ')
248
 
        LOG.debug("members of %s=%s" % (target_group_name, matches))
249
 
        return matches
250
 
 
251
 
    def _is_target_group_member(self, target_group_name, iscsi_target_name):
252
 
        return iscsi_target_name in (
253
 
            self._get_target_group_members(target_group_name))
254
 
 
255
 
    def _get_iscsi_targets(self):
256
 
        (out, _err) = self._execute('/usr/sbin/itadm', 'list-target')
257
 
        matches = _collect_lines(out)
258
 
 
259
 
        # Skip header
260
 
        if len(matches) != 0:
261
 
            assert 'TARGET NAME' in matches[0]
262
 
            matches = matches[1:]
263
 
 
264
 
        targets = []
265
 
        for line in matches:
266
 
            items = line.split()
267
 
            assert len(items) == 3
268
 
            targets.append(items[0])
269
 
 
270
 
        LOG.debug("_get_iscsi_targets=%s" % (targets))
271
 
        return targets
272
 
 
273
 
    def _iscsi_target_exists(self, iscsi_target_name):
274
 
        return iscsi_target_name in self._get_iscsi_targets()
275
 
 
276
 
    def _build_zfs_poolname(self, volume):
277
 
        zfs_poolname = '%s%s' % (FLAGS.san_zfs_volume_base, volume['name'])
278
 
        return zfs_poolname
279
 
 
280
 
    def create_volume(self, volume):
281
 
        """Creates a volume."""
282
 
        if int(volume['size']) == 0:
283
 
            sizestr = '100M'
284
 
        else:
285
 
            sizestr = '%sG' % volume['size']
286
 
 
287
 
        zfs_poolname = self._build_zfs_poolname(volume)
288
 
 
289
 
        # Create a zfs volume
290
 
        cmd = ['/usr/sbin/zfs', 'create']
291
 
        if FLAGS.san_thin_provision:
292
 
            cmd.append('-s')
293
 
        cmd.extend(['-V', sizestr])
294
 
        cmd.append(zfs_poolname)
295
 
        self._execute(*cmd)
296
 
 
297
 
    def _get_luid(self, volume):
298
 
        zfs_poolname = self._build_zfs_poolname(volume)
299
 
        zvol_name = '/dev/zvol/rdsk/%s' % zfs_poolname
300
 
 
301
 
        (out, _err) = self._execute('/usr/sbin/sbdadm', 'list-lu')
302
 
 
303
 
        lines = _collect_lines(out)
304
 
 
305
 
        # Strip headers
306
 
        if len(lines) >= 1:
307
 
            if lines[0] == '':
308
 
                lines = lines[1:]
309
 
 
310
 
        if len(lines) >= 4:
311
 
            assert 'Found' in lines[0]
312
 
            assert '' == lines[1]
313
 
            assert 'GUID' in lines[2]
314
 
            assert '------------------' in lines[3]
315
 
 
316
 
            lines = lines[4:]
317
 
 
318
 
        for line in lines:
319
 
            items = line.split()
320
 
            assert len(items) == 3
321
 
            if items[2] == zvol_name:
322
 
                luid = items[0].strip()
323
 
                return luid
324
 
 
325
 
        msg = _('LUID not found for %(zfs_poolname)s. '
326
 
                'Output=%(out)s') % locals()
327
 
        raise exception.VolumeBackendAPIException(data=msg)
328
 
 
329
 
    def _is_lu_created(self, volume):
330
 
        luid = self._get_luid(volume)
331
 
        return luid
332
 
 
333
 
    def delete_volume(self, volume):
334
 
        """Deletes a volume."""
335
 
        zfs_poolname = self._build_zfs_poolname(volume)
336
 
        self._execute('/usr/sbin/zfs', 'destroy', zfs_poolname)
337
 
 
338
 
    def local_path(self, volume):
339
 
        # TODO(justinsb): Is this needed here?
340
 
        escaped_group = FLAGS.volume_group.replace('-', '--')
341
 
        escaped_name = volume['name'].replace('-', '--')
342
 
        return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)
343
 
 
344
 
    def ensure_export(self, context, volume):
345
 
        """Synchronously recreates an export for a logical volume."""
346
 
        #TODO(justinsb): On bootup, this is called for every volume.
347
 
        # It then runs ~5 SSH commands for each volume,
348
 
        # most of which fetch the same info each time
349
 
        # This makes initial start stupid-slow
350
 
        return self._do_export(volume, force_create=False)
351
 
 
352
 
    def create_export(self, context, volume):
353
 
        return self._do_export(volume, force_create=True)
354
 
 
355
 
    def _do_export(self, volume, force_create):
356
 
        # Create a Logical Unit (LU) backed by the zfs volume
357
 
        zfs_poolname = self._build_zfs_poolname(volume)
358
 
 
359
 
        if force_create or not self._is_lu_created(volume):
360
 
            zvol_name = '/dev/zvol/rdsk/%s' % zfs_poolname
361
 
            self._execute('/usr/sbin/sbdadm', 'create-lu', zvol_name)
362
 
 
363
 
        luid = self._get_luid(volume)
364
 
        iscsi_name = self._build_iscsi_target_name(volume)
365
 
        target_group_name = 'tg-%s' % volume['name']
366
 
 
367
 
        # Create a iSCSI target, mapped to just this volume
368
 
        if force_create or not self._target_group_exists(target_group_name):
369
 
            self._execute('/usr/sbin/stmfadm', 'create-tg', target_group_name)
370
 
 
371
 
        # Yes, we add the initiatior before we create it!
372
 
        # Otherwise, it complains that the target is already active
373
 
        if force_create or not self._is_target_group_member(target_group_name,
374
 
                                                            iscsi_name):
375
 
            self._execute('/usr/sbin/stmfadm',
376
 
                          'add-tg-member', '-g', target_group_name, iscsi_name)
377
 
 
378
 
        if force_create or not self._iscsi_target_exists(iscsi_name):
379
 
            self._execute('/usr/sbin/itadm', 'create-target', '-n', iscsi_name)
380
 
 
381
 
        if force_create or not self._view_exists(luid):
382
 
            self._execute('/usr/sbin/stmfadm',
383
 
                          'add-view', '-t', target_group_name, luid)
384
 
 
385
 
        #TODO(justinsb): Is this always 1? Does it matter?
386
 
        iscsi_portal_interface = '1'
387
 
        iscsi_portal = FLAGS.san_ip + ":3260," + iscsi_portal_interface
388
 
 
389
 
        db_update = {}
390
 
        db_update['provider_location'] = ("%s %s" %
391
 
                                          (iscsi_portal,
392
 
                                           iscsi_name))
393
 
 
394
 
        return db_update
395
 
 
396
 
    def remove_export(self, context, volume):
397
 
        """Removes an export for a logical volume."""
398
 
 
399
 
        # This is the reverse of _do_export
400
 
        luid = self._get_luid(volume)
401
 
        iscsi_name = self._build_iscsi_target_name(volume)
402
 
        target_group_name = 'tg-%s' % volume['name']
403
 
 
404
 
        if self._view_exists(luid):
405
 
            self._execute('/usr/sbin/stmfadm', 'remove-view', '-l', luid, '-a')
406
 
 
407
 
        if self._iscsi_target_exists(iscsi_name):
408
 
            self._execute('/usr/sbin/stmfadm', 'offline-target', iscsi_name)
409
 
            self._execute('/usr/sbin/itadm', 'delete-target', iscsi_name)
410
 
 
411
 
        # We don't delete the tg-member; we delete the whole tg!
412
 
 
413
 
        if self._target_group_exists(target_group_name):
414
 
            self._execute('/usr/sbin/stmfadm', 'delete-tg', target_group_name)
415
 
 
416
 
        if self._is_lu_created(volume):
417
 
            self._execute('/usr/sbin/sbdadm', 'delete-lu', luid)
418
 
 
419
 
 
420
 
class HpSanISCSIDriver(SanISCSIDriver):
421
 
    """Executes commands relating to HP/Lefthand SAN ISCSI volumes.
422
 
 
423
 
    We use the CLIQ interface, over SSH.
424
 
 
425
 
    Rough overview of CLIQ commands used:
426
 
 
427
 
    :createVolume:    (creates the volume)
428
 
 
429
 
    :getVolumeInfo:    (to discover the IQN etc)
430
 
 
431
 
    :getClusterInfo:    (to discover the iSCSI target IP address)
432
 
 
433
 
    :assignVolumeChap:    (exports it with CHAP security)
434
 
 
435
 
    The 'trick' here is that the HP SAN enforces security by default, so
436
 
    normally a volume mount would need both to configure the SAN in the volume
437
 
    layer and do the mount on the compute layer.  Multi-layer operations are
438
 
    not catered for at the moment in the cinder architecture, so instead we
439
 
    share the volume using CHAP at volume creation time.  Then the mount need
440
 
    only use those CHAP credentials, so can take place exclusively in the
441
 
    compute layer.
442
 
    """
443
 
 
444
 
    def _cliq_run(self, verb, cliq_args):
445
 
        """Runs a CLIQ command over SSH, without doing any result parsing"""
446
 
        cliq_arg_strings = []
447
 
        for k, v in cliq_args.items():
448
 
            cliq_arg_strings.append(" %s=%s" % (k, v))
449
 
        cmd = verb + ''.join(cliq_arg_strings)
450
 
 
451
 
        return self._run_ssh(cmd)
452
 
 
453
 
    def _cliq_run_xml(self, verb, cliq_args, check_cliq_result=True):
454
 
        """Runs a CLIQ command over SSH, parsing and checking the output"""
455
 
        cliq_args['output'] = 'XML'
456
 
        (out, _err) = self._cliq_run(verb, cliq_args)
457
 
 
458
 
        LOG.debug(_("CLIQ command returned %s"), out)
459
 
 
460
 
        result_xml = etree.fromstring(out)
461
 
        if check_cliq_result:
462
 
            response_node = result_xml.find("response")
463
 
            if response_node is None:
464
 
                msg = (_("Malformed response to CLIQ command "
465
 
                         "%(verb)s %(cliq_args)s. Result=%(out)s") %
466
 
                       locals())
467
 
                raise exception.VolumeBackendAPIException(data=msg)
468
 
 
469
 
            result_code = response_node.attrib.get("result")
470
 
 
471
 
            if result_code != "0":
472
 
                msg = (_("Error running CLIQ command %(verb)s %(cliq_args)s. "
473
 
                         " Result=%(out)s") %
474
 
                       locals())
475
 
                raise exception.VolumeBackendAPIException(data=msg)
476
 
 
477
 
        return result_xml
478
 
 
479
 
    def _cliq_get_cluster_info(self, cluster_name):
480
 
        """Queries for info about the cluster (including IP)"""
481
 
        cliq_args = {}
482
 
        cliq_args['clusterName'] = cluster_name
483
 
        cliq_args['searchDepth'] = '1'
484
 
        cliq_args['verbose'] = '0'
485
 
 
486
 
        result_xml = self._cliq_run_xml("getClusterInfo", cliq_args)
487
 
 
488
 
        return result_xml
489
 
 
490
 
    def _cliq_get_cluster_vip(self, cluster_name):
491
 
        """Gets the IP on which a cluster shares iSCSI volumes"""
492
 
        cluster_xml = self._cliq_get_cluster_info(cluster_name)
493
 
 
494
 
        vips = []
495
 
        for vip in cluster_xml.findall("response/cluster/vip"):
496
 
            vips.append(vip.attrib.get('ipAddress'))
497
 
 
498
 
        if len(vips) == 1:
499
 
            return vips[0]
500
 
 
501
 
        _xml = etree.tostring(cluster_xml)
502
 
        msg = (_("Unexpected number of virtual ips for cluster "
503
 
                 " %(cluster_name)s. Result=%(_xml)s") %
504
 
               locals())
505
 
        raise exception.VolumeBackendAPIException(data=msg)
506
 
 
507
 
    def _cliq_get_volume_info(self, volume_name):
508
 
        """Gets the volume info, including IQN"""
509
 
        cliq_args = {}
510
 
        cliq_args['volumeName'] = volume_name
511
 
        result_xml = self._cliq_run_xml("getVolumeInfo", cliq_args)
512
 
 
513
 
        # Result looks like this:
514
 
        #<gauche version="1.0">
515
 
        #  <response description="Operation succeeded." name="CliqSuccess"
516
 
        #            processingTime="87" result="0">
517
 
        #    <volume autogrowPages="4" availability="online" blockSize="1024"
518
 
        #       bytesWritten="0" checkSum="false" clusterName="Cluster01"
519
 
        #       created="2011-02-08T19:56:53Z" deleting="false" description=""
520
 
        #       groupName="Group01" initialQuota="536870912" isPrimary="true"
521
 
        #       iscsiIqn="iqn.2003-10.com.lefthandnetworks:group01:25366:vol-b"
522
 
        #       maxSize="6865387257856" md5="9fa5c8b2cca54b2948a63d833097e1ca"
523
 
        #       minReplication="1" name="vol-b" parity="0" replication="2"
524
 
        #       reserveQuota="536870912" scratchQuota="4194304"
525
 
        #       serialNumber="9fa5c8b2cca54b2948a63d833097e1ca0000000000006316"
526
 
        #       size="1073741824" stridePages="32" thinProvision="true">
527
 
        #      <status description="OK" value="2"/>
528
 
        #      <permission access="rw"
529
 
        #            authGroup="api-34281B815713B78-(trimmed)51ADD4B7030853AA7"
530
 
        #            chapName="chapusername" chapRequired="true" id="25369"
531
 
        #            initiatorSecret="" iqn="" iscsiEnabled="true"
532
 
        #            loadBalance="true" targetSecret="supersecret"/>
533
 
        #    </volume>
534
 
        #  </response>
535
 
        #</gauche>
536
 
 
537
 
        # Flatten the nodes into a dictionary; use prefixes to avoid collisions
538
 
        volume_attributes = {}
539
 
 
540
 
        volume_node = result_xml.find("response/volume")
541
 
        for k, v in volume_node.attrib.items():
542
 
            volume_attributes["volume." + k] = v
543
 
 
544
 
        status_node = volume_node.find("status")
545
 
        if not status_node is None:
546
 
            for k, v in status_node.attrib.items():
547
 
                volume_attributes["status." + k] = v
548
 
 
549
 
        # We only consider the first permission node
550
 
        permission_node = volume_node.find("permission")
551
 
        if not permission_node is None:
552
 
            for k, v in status_node.attrib.items():
553
 
                volume_attributes["permission." + k] = v
554
 
 
555
 
        LOG.debug(_("Volume info: %(volume_name)s => %(volume_attributes)s") %
556
 
                  locals())
557
 
        return volume_attributes
558
 
 
559
 
    def create_volume(self, volume):
560
 
        """Creates a volume."""
561
 
        cliq_args = {}
562
 
        cliq_args['clusterName'] = FLAGS.san_clustername
563
 
        #TODO(justinsb): Should we default to inheriting thinProvision?
564
 
        cliq_args['thinProvision'] = '1' if FLAGS.san_thin_provision else '0'
565
 
        cliq_args['volumeName'] = volume['name']
566
 
        if int(volume['size']) == 0:
567
 
            cliq_args['size'] = '100MB'
568
 
        else:
569
 
            cliq_args['size'] = '%sGB' % volume['size']
570
 
 
571
 
        self._cliq_run_xml("createVolume", cliq_args)
572
 
 
573
 
        volume_info = self._cliq_get_volume_info(volume['name'])
574
 
        cluster_name = volume_info['volume.clusterName']
575
 
        iscsi_iqn = volume_info['volume.iscsiIqn']
576
 
 
577
 
        #TODO(justinsb): Is this always 1? Does it matter?
578
 
        cluster_interface = '1'
579
 
 
580
 
        cluster_vip = self._cliq_get_cluster_vip(cluster_name)
581
 
        iscsi_portal = cluster_vip + ":3260," + cluster_interface
582
 
 
583
 
        model_update = {}
584
 
 
585
 
        # NOTE(jdg): LH volumes always at lun 0 ?
586
 
        model_update['provider_location'] = ("%s %s %s" %
587
 
                                             (iscsi_portal,
588
 
                                              iscsi_iqn,
589
 
                                              0))
590
 
 
591
 
        return model_update
592
 
 
593
 
    def create_volume_from_snapshot(self, volume, snapshot):
594
 
        """Creates a volume from a snapshot."""
595
 
        raise NotImplementedError()
596
 
 
597
 
    def create_snapshot(self, snapshot):
598
 
        """Creates a snapshot."""
599
 
        raise NotImplementedError()
600
 
 
601
 
    def delete_volume(self, volume):
602
 
        """Deletes a volume."""
603
 
        cliq_args = {}
604
 
        cliq_args['volumeName'] = volume['name']
605
 
        cliq_args['prompt'] = 'false'  # Don't confirm
606
 
 
607
 
        self._cliq_run_xml("deleteVolume", cliq_args)
608
 
 
609
 
    def local_path(self, volume):
610
 
        # TODO(justinsb): Is this needed here?
611
 
        msg = _("local_path not supported")
612
 
        raise exception.VolumeBackendAPIException(data=msg)
613
 
 
614
 
    def initialize_connection(self, volume, connector):
615
 
        """Assigns the volume to a server.
616
 
 
617
 
        Assign any created volume to a compute node/host so that it can be
618
 
        used from that host. HP VSA requires a volume to be assigned
619
 
        to a server.
620
 
 
621
 
        This driver returns a driver_volume_type of 'iscsi'.
622
 
        The format of the driver data is defined in _get_iscsi_properties.
623
 
        Example return value:
624
 
 
625
 
            {
626
 
                'driver_volume_type': 'iscsi'
627
 
                'data': {
628
 
                    'target_discovered': True,
629
 
                    'target_iqn': 'iqn.2010-10.org.openstack:volume-00000001',
630
 
                    'target_protal': '127.0.0.1:3260',
631
 
                    'volume_id': 1,
632
 
                }
633
 
            }
634
 
 
635
 
        """
636
 
        cliq_args = {}
637
 
        cliq_args['volumeName'] = volume['name']
638
 
        cliq_args['serverName'] = connector['host']
639
 
        self._cliq_run_xml("assignVolumeToServer", cliq_args)
640
 
 
641
 
        iscsi_properties = self._get_iscsi_properties(volume)
642
 
        return {
643
 
            'driver_volume_type': 'iscsi',
644
 
            'data': iscsi_properties
645
 
        }
646
 
 
647
 
    def terminate_connection(self, volume, connector):
648
 
        """Unassign the volume from the host."""
649
 
        cliq_args = {}
650
 
        cliq_args['volumeName'] = volume['name']
651
 
        cliq_args['serverName'] = connector['host']
652
 
        self._cliq_run_xml("unassignVolumeToServer", cliq_args)