42 help='The base dataset for ZFS volumes.'), ] |
47 help='The base dataset for ZFS volumes.'), ] |
43 |
48 |
44 FLAGS.register_opts(solaris_zfs_opts) |
49 FLAGS.register_opts(solaris_zfs_opts) |
45 |
50 |
46 |
51 |
47 class ZFSVolumeDriver(driver.VolumeDriver): |
52 class ZFSVolumeDriver(SanDriver): |
48 """Local ZFS volume operations.""" |
53 """Local ZFS volume operations.""" |
49 protocol = 'local' |
54 protocol = 'local' |
50 |
55 |
51 def __init__(self, *args, **kwargs): |
56 def __init__(self, *args, **kwargs): |
52 super(ZFSVolumeDriver, self).__init__(*args, **kwargs) |
57 super(ZFSVolumeDriver, self).__init__(execute=self.solaris_execute, |
|
58 *args, **kwargs) |
53 self.configuration.append_config_values(solaris_zfs_opts) |
59 self.configuration.append_config_values(solaris_zfs_opts) |
|
60 self.run_local = self.configuration.san_is_local |
|
61 self.hostname = socket.gethostname() |
|
62 |
|
63 def solaris_execute(self, *cmd, **kwargs): |
|
64 """Execute the command locally or remotely.""" |
|
65 if self.run_local: |
|
66 return processutils.execute(*cmd, **kwargs) |
|
67 else: |
|
68 return super(ZFSVolumeDriver, self)._run_ssh(cmd, |
|
69 check_exit_code=True) |
54 |
70 |
55 def check_for_setup_error(self): |
71 def check_for_setup_error(self): |
56 """Check the setup error.""" |
72 """Check the setup error.""" |
57 pass |
73 pass |
58 |
74 |
59 def create_volume(self, volume): |
75 def create_volume(self, volume): |
60 """Create a volume.""" |
76 """Create a volume.""" |
61 size = '%sG' % volume['size'] |
77 size = '%sG' % volume['size'] |
62 zfs_volume = self._get_zfs_volume_name(volume) |
78 zfs_volume = self._get_zfs_volume_name(volume['name']) |
63 |
79 |
64 # Create a ZFS volume |
80 # Create a ZFS volume |
65 cmd = ['/usr/sbin/zfs', 'create', '-V', size, zfs_volume] |
81 cmd = ['/usr/sbin/zfs', 'create', '-V', size, zfs_volume] |
66 self._execute(*cmd) |
82 self._execute(*cmd) |
67 LOG.debug(_("Created ZFS volume '%s'") % volume['name']) |
83 LOG.debug(_("Created ZFS volume '%s'") % volume['name']) |
203 |
219 |
204 return self._stats |
220 return self._stats |
205 |
221 |
206 def copy_image_to_volume(self, context, volume, image_service, image_id): |
222 def copy_image_to_volume(self, context, volume, image_service, image_id): |
207 """Fetch the image from image_service and write it to the volume.""" |
223 """Fetch the image from image_service and write it to the volume.""" |
208 image_utils.fetch_to_raw(context, |
224 raise NotImplementedError() |
209 image_service, |
|
210 image_id, |
|
211 self.local_path(volume)) |
|
212 |
225 |
213 def copy_volume_to_image(self, context, volume, image_service, image_meta): |
226 def copy_volume_to_image(self, context, volume, image_service, image_meta): |
214 """Copy the volume to the specified image.""" |
227 """Copy the volume to the specified image.""" |
215 image_utils.upload_volume(context, |
228 raise NotImplementedError() |
216 image_service, |
|
217 image_meta, |
|
218 self.local_path(volume)) |
|
219 |
229 |
220 def _get_zfs_property(self, prop, dataset): |
230 def _get_zfs_property(self, prop, dataset): |
221 """Get the value of property for the dataset.""" |
231 """Get the value of property for the dataset.""" |
222 (out, _err) = self._execute('/usr/sbin/zfs', 'get', '-H', '-o', |
232 try: |
223 'value', prop, dataset) |
233 (out, _err) = self._execute('/usr/sbin/zfs', 'get', '-H', '-o', |
224 return out.rstrip() |
234 'value', prop, dataset) |
|
235 return out.rstrip() |
|
236 except processutils.ProcessExecutionError: |
|
237 LOG.info(_("Failed to get the property '%s' of the dataset '%s'") % |
|
238 (prop, dataset)) |
|
239 return None |
225 |
240 |
226 def _get_zfs_snap_name(self, snapshot): |
241 def _get_zfs_snap_name(self, snapshot): |
227 """Get the snapshot path.""" |
242 """Get the snapshot path.""" |
228 return "%s/%s@%s" % (self.configuration.zfs_volume_base, |
243 return "%s/%s@%s" % (self.configuration.zfs_volume_base, |
229 snapshot['volume_name'], snapshot['name']) |
244 snapshot['volume_name'], snapshot['name']) |
230 |
245 |
231 def _get_zfs_volume_name(self, volume): |
246 def _get_zfs_volume_name(self, volume_name): |
232 """Add the pool name to get the ZFS volume.""" |
247 """Add the pool name to get the ZFS volume.""" |
233 return "%s/%s" % (self.configuration.zfs_volume_base, |
248 return "%s/%s" % (self.configuration.zfs_volume_base, |
234 volume['name']) |
249 volume_name) |
|
250 |
|
251 def _piped_execute(self, cmd1, cmd2): |
|
252 """Pipe output of cmd1 into cmd2.""" |
|
253 LOG.debug(_("Piping cmd1='%s' into cmd2='%s'") % |
|
254 (' '.join(cmd1), ' '.join(cmd2))) |
|
255 |
|
256 try: |
|
257 p1 = subprocess.Popen(cmd1, stdout=subprocess.PIPE, |
|
258 stderr=subprocess.PIPE) |
|
259 except: |
|
260 LOG.error(_("_piped_execute '%s' failed.") % (cmd1)) |
|
261 raise |
|
262 |
|
263 # Set the pipe to be blocking because evenlet.green.subprocess uses |
|
264 # the non-blocking pipe. |
|
265 flags = fcntl.fcntl(p1.stdout, fcntl.F_GETFL) & (~os.O_NONBLOCK) |
|
266 fcntl.fcntl(p1.stdout, fcntl.F_SETFL, flags) |
|
267 |
|
268 p2 = subprocess.Popen(cmd2, stdin=p1.stdout, |
|
269 stdout=subprocess.PIPE, |
|
270 stderr=subprocess.PIPE) |
|
271 p1.stdout.close() |
|
272 stdout, stderr = p2.communicate() |
|
273 if p2.returncode: |
|
274 msg = (_("_piped_execute failed with the info '%s' and '%s'.") % |
|
275 (stdout, stderr)) |
|
276 raise exception.VolumeBackendAPIException(data=msg) |
|
277 |
|
278 def _zfs_send_recv(self, src, dst, remote=False): |
|
279 """Replicate the ZFS dataset by calling zfs send/recv cmd""" |
|
280 src_snapshot = {'volume_name': src['name'], |
|
281 'name': 'tmp-snapshot-%s' % src['id']} |
|
282 src_snapshot_name = self._get_zfs_snap_name(src_snapshot) |
|
283 prop_type = self._get_zfs_property('type', src_snapshot_name) |
|
284 # Delete the temporary snapshot if it already exists |
|
285 if prop_type == 'snapshot': |
|
286 self.delete_snapshot(src_snapshot) |
|
287 # Create a temporary snapshot of volume |
|
288 self.create_snapshot(src_snapshot) |
|
289 src_snapshot_name = self._get_zfs_snap_name(src_snapshot) |
|
290 |
|
291 cmd1 = ['/usr/sbin/zfs', 'send', src_snapshot_name] |
|
292 cmd2 = ['/usr/sbin/zfs', 'receive', dst] |
|
293 self._piped_execute(cmd1, cmd2) |
|
294 |
|
295 # Delete the temporary src snapshot and dst snapshot |
|
296 self.delete_snapshot(src_snapshot) |
|
297 dst_snapshot_name = "%s@tmp-snapshot-%s" % (dst, src['id']) |
|
298 cmd = ['/usr/sbin/zfs', 'destroy', dst_snapshot_name] |
|
299 self._execute(*cmd) |
235 |
300 |
236 def _get_zvol_path(self, volume): |
301 def _get_zvol_path(self, volume): |
237 """Get the ZFS volume path.""" |
302 """Get the ZFS volume path.""" |
238 return "/dev/zvol/rdsk/%s" % self._get_zfs_volume_name(volume) |
303 return "/dev/zvol/rdsk/%s" % self._get_zfs_volume_name(volume['name']) |
239 |
304 |
240 def _update_volume_stats(self): |
305 def _update_volume_stats(self): |
241 """Retrieve volume status info.""" |
306 """Retrieve volume status info.""" |
242 |
307 |
243 LOG.debug(_("Updating volume status")) |
308 LOG.debug(_("Updating volume status")) |
254 avail_size = self._get_zfs_property('avail', dataset) |
319 avail_size = self._get_zfs_property('avail', dataset) |
255 stats['total_capacity_gb'] = \ |
320 stats['total_capacity_gb'] = \ |
256 (Size(used_size) + Size(avail_size)).get(Size.gb_units) |
321 (Size(used_size) + Size(avail_size)).get(Size.gb_units) |
257 stats['free_capacity_gb'] = Size(avail_size).get(Size.gb_units) |
322 stats['free_capacity_gb'] = Size(avail_size).get(Size.gb_units) |
258 stats['reserved_percentage'] = self.configuration.reserved_percentage |
323 stats['reserved_percentage'] = self.configuration.reserved_percentage |
|
324 stats['location_info'] =\ |
|
325 ('ZFSVolumeDriver:%(hostname)s:%(zfs_volume_base)s' % |
|
326 {'hostname': self.hostname, |
|
327 'zfs_volume_base': self.configuration.zfs_volume_base}) |
259 |
328 |
260 self._stats = stats |
329 self._stats = stats |
261 |
330 |
262 def extend_volume(self, volume, new_size): |
331 def extend_volume(self, volume, new_size): |
263 """Extend an existing volume's size.""" |
332 """Extend an existing volume's size.""" |
264 volsize_str = 'volsize=%sg' % new_size |
333 volsize_str = 'volsize=%sg' % new_size |
265 zfs_volume = self._get_zfs_volume_name(volume) |
334 zfs_volume = self._get_zfs_volume_name(volume['name']) |
266 try: |
335 try: |
267 self._execute('/usr/sbin/zfs', 'set', volsize_str, zfs_volume) |
336 self._execute('/usr/sbin/zfs', 'set', volsize_str, zfs_volume) |
268 except Exception: |
337 except Exception: |
269 msg = (_("Failed to extend volume size to %(new_size)s GB.") |
338 msg = (_("Failed to extend volume size to %(new_size)s GB.") |
270 % {'new_size': new_size}) |
339 % {'new_size': new_size}) |
271 raise exception.VolumeBackendAPIException(data=msg) |
340 raise exception.VolumeBackendAPIException(data=msg) |
|
341 |
|
342 def rename_volume(self, src, dst): |
|
343 """Rename the volume from src to dst in the same zpool.""" |
|
344 cmd = ['/usr/sbin/zfs', 'rename', src, dst] |
|
345 self._execute(*cmd) |
|
346 |
|
347 LOG.debug(_("Rename the volume '%s' to '%s'") % (src, dst)) |
|
348 |
|
349 def migrate_volume(self, context, volume, host): |
|
350 """Migrate the volume among different backends on the same server. |
|
351 |
|
352 The volume migration can only run locally by calling zfs send/recv |
|
353 cmds and the specified host needs to be on the same server with the |
|
354 host. But, one exception is when the src and dst volume are located |
|
355 under the same zpool locally or remotely, the migration will be done |
|
356 by just renaming the volume. |
|
357 :param context: context |
|
358 :param volume: a dictionary describing the volume to migrate |
|
359 :param host: a dictionary describing the host to migrate to |
|
360 """ |
|
361 false_ret = (False, None) |
|
362 if volume['status'] != 'available': |
|
363 LOG.debug(_("Status of volume '%s' is '%s', not 'available'.") % |
|
364 (volume['name'], volume['status'])) |
|
365 return false_ret |
|
366 |
|
367 if 'capabilities' not in host or \ |
|
368 'location_info' not in host['capabilities']: |
|
369 LOG.debug(_("No location_info or capabilities are in host info")) |
|
370 return false_ret |
|
371 |
|
372 info = host['capabilities']['location_info'] |
|
373 if (self.hostname != info.split(':')[1]): |
|
374 LOG.debug(_("Migration between two different servers '%s' and " |
|
375 "'%s' is not supported yet.") % |
|
376 (self.hostname, info.split(':')[1])) |
|
377 return false_ret |
|
378 |
|
379 dst_volume = "%s/%s" % (info.split(':')[-1], volume['name']) |
|
380 src_volume = self._get_zfs_volume_name(volume['name']) |
|
381 # check if the src and dst volume are under the same zpool |
|
382 if (src_volume.split('/')[0] == dst_volume.split('/')[0]): |
|
383 self.rename_volume(src_volume, dst_volume) |
|
384 else: |
|
385 self._zfs_send_recv(volume, dst_volume) |
|
386 # delete the source volume |
|
387 self.delete_volume(volume) |
|
388 |
|
389 provider_location = {} |
|
390 return (True, provider_location) |
272 |
391 |
273 |
392 |
274 class STMFDriver(ZFSVolumeDriver): |
393 class STMFDriver(ZFSVolumeDriver): |
275 """Abstract base class for common COMSTAR operations.""" |
394 """Abstract base class for common COMSTAR operations.""" |
276 __metaclass__ = abc.ABCMeta |
395 __metaclass__ = abc.ABCMeta |