1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright (c) 2012 NetApp, Inc.
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
10
# http://www.apache.org/licenses/LICENSE-2.0
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
18
Volume driver for NetApp NFS storage.
25
from nova import exception
26
from nova import flags
27
from nova.openstack.common import cfg
28
from nova.openstack.common import log as logging
29
from nova.volume.netapp import netapp_opts
30
from nova.volume import nfs
32
from suds.sax import text
34
LOG = logging.getLogger(__name__)
37
cfg.IntOpt('synchronous_snapshot_create',
39
help='Does snapshot creation call returns immediately')
43
FLAGS.register_opts(netapp_opts)
44
FLAGS.register_opts(netapp_nfs_opts)
47
class NetAppNFSDriver(nfs.NfsDriver):
48
"""Executes commands relating to Volumes."""
49
def __init__(self, *args, **kwargs):
50
# NOTE(vish): db is set by Manager
53
super(NetAppNFSDriver, self).__init__(*args, **kwargs)
55
def set_execute(self, execute):
56
self._execute = execute
58
def do_setup(self, context):
59
self._context = context
60
self.check_for_setup_error()
61
self._client = NetAppNFSDriver._get_client()
63
def check_for_setup_error(self):
64
"""Returns an error if prerequisites aren't met"""
65
NetAppNFSDriver._check_dfm_flags()
66
super(NetAppNFSDriver, self).check_for_setup_error()
68
def create_volume_from_snapshot(self, volume, snapshot):
69
"""Creates a volume from a snapshot."""
70
vol_size = volume.size
71
snap_size = snapshot.volume_size
73
if vol_size != snap_size:
74
msg = _('Cannot create volume of size %(vol_size)s from '
75
'snapshot of size %(snap_size)s')
76
raise exception.NovaException(msg % locals())
78
self._clone_volume(snapshot.name, volume.name, snapshot.volume_id)
79
share = self._get_volume_location(snapshot.volume_id)
81
return {'provider_location': share}
83
def create_snapshot(self, snapshot):
84
"""Creates a snapshot."""
85
self._clone_volume(snapshot['volume_name'],
87
snapshot['volume_id'])
89
def delete_snapshot(self, snapshot):
90
"""Deletes a snapshot."""
91
nfs_mount = self._get_provider_location(snapshot.volume_id)
93
if self._volume_not_present(nfs_mount, snapshot.name):
96
self._execute('rm', self._get_volume_path(nfs_mount, snapshot.name),
100
def _check_dfm_flags():
101
"""Raises error if any required configuration flag for OnCommand proxy
103
required_flags = ['netapp_wsdl_url',
106
'netapp_server_hostname',
107
'netapp_server_port']
108
for flag in required_flags:
109
if not getattr(FLAGS, flag, None):
110
raise exception.NovaException(_('%s is not set') % flag)
114
"""Creates SOAP _client for ONTAP-7 DataFabric Service."""
115
client = suds.client.Client(FLAGS.netapp_wsdl_url,
116
username=FLAGS.netapp_login,
117
password=FLAGS.netapp_password)
118
soap_url = 'http://%s:%s/apis/soap/v1' % (
119
FLAGS.netapp_server_hostname,
120
FLAGS.netapp_server_port)
121
client.set_options(location=soap_url)
125
def _get_volume_location(self, volume_id):
126
"""Returns NFS mount address as <nfs_ip_address>:<nfs_mount_dir>"""
127
nfs_server_ip = self._get_host_ip(volume_id)
128
export_path = self._get_export_path(volume_id)
129
return (nfs_server_ip + ':' + export_path)
131
def _clone_volume(self, volume_name, clone_name, volume_id):
132
"""Clones mounted volume with OnCommand proxy API"""
133
host_id = self._get_host_id(volume_id)
134
export_path = self._get_full_export_path(volume_id, host_id)
136
request = self._client.factory.create('Request')
137
request.Name = 'clone-start'
139
clone_start_args = ('<source-path>%s/%s</source-path>'
140
'<destination-path>%s/%s</destination-path>')
142
request.Args = text.Raw(clone_start_args % (export_path,
147
resp = self._client.service.ApiProxy(Target=host_id,
150
if resp.Status == 'passed' and FLAGS.synchronous_snapshot_create:
151
clone_id = resp.Results['clone-id'][0]
152
clone_id_info = clone_id['clone-id-info'][0]
153
clone_operation_id = int(clone_id_info['clone-op-id'][0])
155
self._wait_for_clone_finished(clone_operation_id, host_id)
156
elif resp.Status == 'failed':
157
raise exception.NovaException(resp.Reason)
159
def _wait_for_clone_finished(self, clone_operation_id, host_id):
161
Polls ONTAP7 for clone status. Returns once clone is finished.
162
:param clone_operation_id: Identifier of ONTAP clone operation
164
clone_list_options = ('<clone-id>'
166
'<clone-op-id>%d</clone-op-id>'
167
'<volume-uuid></volume-uuid>'
171
request = self._client.factory.create('Request')
172
request.Name = 'clone-list-status'
173
request.Args = text.Raw(clone_list_options % clone_operation_id)
175
resp = self._client.service.ApiProxy(Target=host_id, Request=request)
177
while resp.Status != 'passed':
179
resp = self._client.service.ApiProxy(Target=host_id,
182
def _get_provider_location(self, volume_id):
184
Returns provider location for given volume
187
volume = self.db.volume_get(self._context, volume_id)
188
return volume.provider_location
190
def _get_host_ip(self, volume_id):
191
"""Returns IP address for the given volume"""
192
return self._get_provider_location(volume_id).split(':')[0]
194
def _get_export_path(self, volume_id):
195
"""Returns NFS export path for the given volume"""
196
return self._get_provider_location(volume_id).split(':')[1]
198
def _get_host_id(self, volume_id):
199
"""Returns ID of the ONTAP-7 host"""
200
host_ip = self._get_host_ip(volume_id)
201
server = self._client.service
203
resp = server.HostListInfoIterStart(ObjectNameOrId=host_ip)
207
res = server.HostListInfoIterNext(Tag=tag, Maximum=1)
208
if hasattr(res, 'Hosts') and res.Hosts.HostInfo:
209
return res.Hosts.HostInfo[0].HostId
211
server.HostListInfoIterEnd(Tag=tag)
213
def _get_full_export_path(self, volume_id, host_id):
214
"""Returns full path to the NFS share, e.g. /vol/vol0/home"""
215
export_path = self._get_export_path(volume_id)
216
command_args = '<pathname>%s</pathname>'
218
request = self._client.factory.create('Request')
219
request.Name = 'nfs-exportfs-storage-path'
220
request.Args = text.Raw(command_args % export_path)
222
resp = self._client.service.ApiProxy(Target=host_id,
225
if resp.Status == 'passed':
226
return resp.Results['actual-pathname'][0]
227
elif resp.Status == 'failed':
228
raise exception.NovaException(resp.Reason)
230
def _volume_not_present(self, nfs_mount, volume_name):
232
Check if volume exists
235
self._try_execute('ls', self._get_volume_path(nfs_mount,
237
except exception.ProcessExecutionError:
238
# If the volume isn't present
242
def _try_execute(self, *command, **kwargs):
243
# NOTE(vish): Volume commands can partially fail due to timing, but
244
# running them a second time on failure will usually
249
self._execute(*command, **kwargs)
251
except exception.ProcessExecutionError:
253
if tries >= FLAGS.num_shell_tries:
255
LOG.exception(_("Recovering from a failed execute. "
256
"Try number %s"), tries)
257
time.sleep(tries ** 2)
259
def _get_volume_path(self, nfs_share, volume_name):
260
"""Get volume path (local fs path) for given volume name on given nfs
262
@param nfs_share string, example 172.18.194.100:/var/nfs
263
@param volume_name string,
264
example volume-91ee65ec-c473-4391-8c09-162b00c68a8c
266
return os.path.join(self._get_mount_point_for_share(nfs_share),