summaryrefslogtreecommitdiffstats
path: root/nova/volume
diff options
context:
space:
mode:
authorBen Swartzlander <bswartz@netapp.com>2012-09-01 23:39:39 -0400
committerBen Swartzlander <bswartz@netapp.com>2012-09-12 12:43:44 -0400
commit772c5d47d5bdffcd4ff8e09f4116d22568bf6eb9 (patch)
treec2a11bc6bb2dbe9e78e1f6f603e86042a5bb5087 /nova/volume
parent76d094eeba1bcbba16d24e40aea24bb7729b4a30 (diff)
Backport changes from Cinder to Nova-Volume
NetApp C-mode driver. Generic NFS-based block device driver. NetApp NFS-based block device driver. blueprint netapp-volume-driver-cmode blueprint nfs-files-as-virtual-block-devices blueprint netapp-nfs-cinder-driver bug 1037619 bug 1037622 Change-Id: I513c3f88bcb03f3b71a453f92f5912d7730a8bbc
Diffstat (limited to 'nova/volume')
-rw-r--r--nova/volume/netapp.py294
-rw-r--r--nova/volume/netapp_nfs.py267
-rw-r--r--nova/volume/nfs.py293
3 files changed, 854 insertions, 0 deletions
diff --git a/nova/volume/netapp.py b/nova/volume/netapp.py
index 6dd5c0e31..ce62a33ac 100644
--- a/nova/volume/netapp.py
+++ b/nova/volume/netapp.py
@@ -994,3 +994,297 @@ class NetAppISCSIDriver(driver.ISCSIDriver):
def check_for_export(self, context, volume_id):
raise NotImplementedError()
+
+
+class NetAppLun(object):
+ """Represents a LUN on NetApp storage."""
+
+ def __init__(self, handle, name, size, metadata_dict):
+ self.handle = handle
+ self.name = name
+ self.size = size
+ self.metadata = metadata_dict
+
+ def get_metadata_property(self, prop):
+ """Get the metadata property of a LUN."""
+ if prop in self.metadata:
+ return self.metadata[prop]
+ name = self.name
+ msg = _("No metadata property %(prop)s defined for the LUN %(name)s")
+ LOG.debug(msg % locals())
+
+
+class NetAppCmodeISCSIDriver(driver.ISCSIDriver):
+ """NetApp C-mode iSCSI volume driver."""
+
+ def __init__(self, *args, **kwargs):
+ super(NetAppCmodeISCSIDriver, self).__init__(*args, **kwargs)
+ self.lun_table = {}
+
+ def _create_client(self, **kwargs):
+ """Instantiate a web services client.
+
+ This method creates a "suds" client to make web services calls to the
+ DFM server. Note that the WSDL file is quite large and may take
+ a few seconds to parse.
+ """
+ wsdl_url = kwargs['wsdl_url']
+ LOG.debug(_('Using WSDL: %s') % wsdl_url)
+ if kwargs['cache']:
+ self.client = client.Client(wsdl_url, username=kwargs['login'],
+ password=kwargs['password'])
+ else:
+ self.client = client.Client(wsdl_url, username=kwargs['login'],
+ password=kwargs['password'],
+ cache=None)
+
+ def _check_flags(self):
+ """Ensure that the flags we care about are set."""
+ required_flags = ['netapp_wsdl_url', 'netapp_login', 'netapp_password',
+ 'netapp_server_hostname', 'netapp_server_port']
+ for flag in required_flags:
+ if not getattr(FLAGS, flag, None):
+ msg = _('%s is not set') % flag
+ raise exception.InvalidInput(data=msg)
+
+ def do_setup(self, context):
+ """Setup the NetApp Volume driver.
+
+ Called one time by the manager after the driver is loaded.
+ Validate the flags we care about and setup the suds (web services)
+ client.
+ """
+ self._check_flags()
+ self._create_client(wsdl_url=FLAGS.netapp_wsdl_url,
+ login=FLAGS.netapp_login, password=FLAGS.netapp_password,
+ hostname=FLAGS.netapp_server_hostname,
+ port=FLAGS.netapp_server_port, cache=True)
+
+ def check_for_setup_error(self):
+ """Check that the driver is working and can communicate.
+
+ Discovers the LUNs on the NetApp server.
+ """
+ self.lun_table = {}
+ luns = self.client.service.ListLuns()
+ for lun in luns:
+ meta_dict = {}
+ if hasattr(lun, 'Metadata'):
+ meta_dict = self._create_dict_from_meta(lun.Metadata)
+ discovered_lun = NetAppLun(lun.Handle, lun.Name, lun.Size,
+ meta_dict)
+ self._add_lun_to_table(discovered_lun)
+ LOG.debug(_("Success getting LUN list from server"))
+
+ def create_volume(self, volume):
+ """Driver entry point for creating a new volume."""
+ default_size = '104857600' # 100 MB
+ gigabytes = 1073741824L # 2^30
+ name = volume['name']
+ if int(volume['size']) == 0:
+ size = default_size
+ else:
+ size = str(int(volume['size']) * gigabytes)
+ extra_args = {}
+ extra_args['OsType'] = 'linux'
+ extra_args['QosType'] = self._get_qos_type(volume)
+ extra_args['Container'] = volume['project_id']
+ extra_args['Display'] = volume['display_name']
+ extra_args['Description'] = volume['display_description']
+ extra_args['SpaceReserved'] = True
+ server = self.client.service
+ metadata = self._create_metadata_list(extra_args)
+ lun = server.ProvisionLun(Name=name, Size=size,
+ Metadata=metadata)
+ LOG.debug(_("Created LUN with name %s") % name)
+ self._add_lun_to_table(NetAppLun(lun.Handle, lun.Name,
+ lun.Size, self._create_dict_from_meta(lun.Metadata)))
+
+ def delete_volume(self, volume):
+ """Driver entry point for destroying existing volumes."""
+ name = volume['name']
+ handle = self._get_lun_handle(name)
+ self.client.service.DestroyLun(Handle=handle)
+ LOG.debug(_("Destroyed LUN %s") % handle)
+ self.lun_table.pop(name)
+
+ def ensure_export(self, context, volume):
+ """Driver entry point to get the export info for an existing volume."""
+ handle = self._get_lun_handle(volume['name'])
+ return {'provider_location': handle}
+
+ def create_export(self, context, volume):
+ """Driver entry point to get the export info for a new volume."""
+ handle = self._get_lun_handle(volume['name'])
+ return {'provider_location': handle}
+
+ def remove_export(self, context, volume):
+ """Driver exntry point to remove an export for a volume.
+
+ Since exporting is idempotent in this driver, we have nothing
+ to do for unexporting.
+ """
+ pass
+
+ def initialize_connection(self, volume, connector):
+ """Driver entry point to attach a volume to an instance.
+
+ Do the LUN masking on the storage system so the initiator can access
+ the LUN on the target. Also return the iSCSI properties so the
+ initiator can find the LUN. This implementation does not call
+ _get_iscsi_properties() to get the properties because cannot store the
+ LUN number in the database. We only find out what the LUN number will
+ be during this method call so we construct the properties dictionary
+ ourselves.
+ """
+ initiator_name = connector['initiator']
+ handle = volume['provider_location']
+ server = self.client.service
+ server.MapLun(Handle=handle, InitiatorType="iscsi",
+ InitiatorName=initiator_name)
+ msg = _("Mapped LUN %(handle)s to the initiator %(initiator_name)s")
+ LOG.debug(msg % locals())
+
+ target_details_list = server.GetLunTargetDetails(Handle=handle,
+ InitiatorType="iscsi", InitiatorName=initiator_name)
+ msg = _("Succesfully fetched target details for LUN %(handle)s and "
+ "initiator %(initiator_name)s")
+ LOG.debug(msg % locals())
+
+ if not target_details_list:
+ msg = _('Failed to get LUN target details for the LUN %s')
+ raise exception.VolumeBackendAPIException(msg % handle)
+ target_details = target_details_list[0]
+ if not target_details.Address and target_details.Port:
+ msg = _('Failed to get target portal for the LUN %s')
+ raise exception.VolumeBackendAPIException(msg % handle)
+ iqn = target_details.Iqn
+ if not iqn:
+ msg = _('Failed to get target IQN for the LUN %s')
+ raise exception.VolumeBackendAPIException(msg % handle)
+
+ properties = {}
+ properties['target_discovered'] = False
+ (address, port) = (target_details.Address, target_details.Port)
+ properties['target_portal'] = '%s:%s' % (address, port)
+ properties['target_iqn'] = iqn
+ properties['target_lun'] = target_details.LunNumber
+ properties['volume_id'] = volume['id']
+
+ auth = volume['provider_auth']
+ if auth:
+ (auth_method, auth_username, auth_secret) = auth.split()
+ properties['auth_method'] = auth_method
+ properties['auth_username'] = auth_username
+ properties['auth_password'] = auth_secret
+
+ return {
+ 'driver_volume_type': 'iscsi',
+ 'data': properties,
+ }
+
+ def terminate_connection(self, volume, connector):
+ """Driver entry point to unattach a volume from an instance.
+
+ Unmask the LUN on the storage system so the given intiator can no
+ longer access it.
+ """
+ initiator_name = connector['initiator']
+ handle = volume['provider_location']
+ self.client.service.UnmapLun(Handle=handle, InitiatorType="iscsi",
+ InitiatorName=initiator_name)
+ msg = _("Unmapped LUN %(handle)s from the initiator "
+ "%(initiator_name)s")
+ LOG.debug(msg % locals())
+
+ def create_snapshot(self, snapshot):
+ """Driver entry point for creating a snapshot.
+
+ This driver implements snapshots by using efficient single-file
+ (LUN) cloning.
+ """
+ vol_name = snapshot['volume_name']
+ snapshot_name = snapshot['name']
+ lun = self.lun_table[vol_name]
+ extra_args = {'SpaceReserved': False}
+ self._clone_lun(lun.handle, snapshot_name, extra_args)
+
+ def delete_snapshot(self, snapshot):
+ """Driver entry point for deleting a snapshot."""
+ handle = self._get_lun_handle(snapshot['name'])
+ self.client.service.DestroyLun(Handle=handle)
+ LOG.debug(_("Destroyed LUN %s") % handle)
+
+ def create_volume_from_snapshot(self, volume, snapshot):
+ """Driver entry point for creating a new volume from a snapshot.
+
+ Many would call this "cloning" and in fact we use cloning to implement
+ this feature.
+ """
+ snapshot_name = snapshot['name']
+ lun = self.lun_table[snapshot_name]
+ new_name = volume['name']
+ extra_args = {}
+ extra_args['OsType'] = 'linux'
+ extra_args['QosType'] = self._get_qos_type(volume)
+ extra_args['Container'] = volume['project_id']
+ extra_args['Display'] = volume['display_name']
+ extra_args['Description'] = volume['display_description']
+ extra_args['SpaceReserved'] = True
+ self._clone_lun(lun.handle, new_name, extra_args)
+
+ def check_for_export(self, context, volume_id):
+ raise NotImplementedError()
+
+ def _get_qos_type(self, volume):
+ """Get the storage service type for a volume."""
+ type_id = volume['volume_type_id']
+ if not type_id:
+ return None
+ volume_type = volume_types.get_volume_type(None, type_id)
+ if not volume_type:
+ return None
+ return volume_type['name']
+
+ def _add_lun_to_table(self, lun):
+ """Adds LUN to cache table."""
+ if not isinstance(lun, NetAppLun):
+ msg = _("Object is not a NetApp LUN.")
+ raise exception.VolumeBackendAPIException(data=msg)
+ self.lun_table[lun.name] = lun
+
+ def _clone_lun(self, handle, new_name, extra_args):
+ """Clone LUN with the given handle to the new name."""
+ server = self.client.service
+ metadata = self._create_metadata_list(extra_args)
+ lun = server.CloneLun(Handle=handle, NewName=new_name,
+ Metadata=metadata)
+ LOG.debug(_("Cloned LUN with new name %s") % new_name)
+ self._add_lun_to_table(NetAppLun(lun.Handle, lun.Name,
+ lun.Size, self._create_dict_from_meta(lun.Metadata)))
+
+ def _create_metadata_list(self, extra_args):
+ """Creates metadata from kwargs."""
+ metadata = []
+ for key in extra_args.keys():
+ meta = self.client.factory.create("Metadata")
+ meta.Key = key
+ meta.Value = extra_args[key]
+ metadata.append(meta)
+ return metadata
+
+ def _get_lun_handle(self, name):
+ """Get the details for a LUN from our cache table."""
+ if not name in self.lun_table:
+ LOG.warn(_("Could not find handle for LUN named %s") % name)
+ return None
+ return self.lun_table[name]
+
+ def _create_dict_from_meta(self, metadata):
+ """Creates dictionary from metadata array."""
+ meta_dict = {}
+ if not metadata:
+ return meta_dict
+ for meta in metadata:
+ meta_dict[meta.Key] = meta.Value
+ return meta_dict
diff --git a/nova/volume/netapp_nfs.py b/nova/volume/netapp_nfs.py
new file mode 100644
index 000000000..27d278aa3
--- /dev/null
+++ b/nova/volume/netapp_nfs.py
@@ -0,0 +1,267 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2012 NetApp, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""
+Volume driver for NetApp NFS storage.
+"""
+
+import os
+import suds
+import time
+
+from nova import exception
+from nova import flags
+from nova.openstack.common import cfg
+from nova.openstack.common import log as logging
+from nova.volume.netapp import netapp_opts
+from nova.volume import nfs
+
+from suds.sax import text
+
+LOG = logging.getLogger(__name__)
+
+netapp_nfs_opts = [
+ cfg.IntOpt('synchronous_snapshot_create',
+ default=0,
+ help='Does snapshot creation call returns immediately')
+ ]
+
+FLAGS = flags.FLAGS
+FLAGS.register_opts(netapp_opts)
+FLAGS.register_opts(netapp_nfs_opts)
+
+
+class NetAppNFSDriver(nfs.NfsDriver):
+ """Executes commands relating to Volumes."""
+ def __init__(self, *args, **kwargs):
+ # NOTE(vish): db is set by Manager
+ self._execute = None
+ self._context = None
+ super(NetAppNFSDriver, self).__init__(*args, **kwargs)
+
+ def set_execute(self, execute):
+ self._execute = execute
+
+ def do_setup(self, context):
+ self._context = context
+ self.check_for_setup_error()
+ self._client = NetAppNFSDriver._get_client()
+
+ def check_for_setup_error(self):
+ """Returns an error if prerequisites aren't met"""
+ NetAppNFSDriver._check_dfm_flags()
+ super(NetAppNFSDriver, self).check_for_setup_error()
+
+ def create_volume_from_snapshot(self, volume, snapshot):
+ """Creates a volume from a snapshot."""
+ vol_size = volume.size
+ snap_size = snapshot.volume_size
+
+ if vol_size != snap_size:
+ msg = _('Cannot create volume of size %(vol_size)s from '
+ 'snapshot of size %(snap_size)s')
+ raise exception.NovaException(msg % locals())
+
+ self._clone_volume(snapshot.name, volume.name, snapshot.volume_id)
+ share = self._get_volume_location(snapshot.volume_id)
+
+ return {'provider_location': share}
+
+ def create_snapshot(self, snapshot):
+ """Creates a snapshot."""
+ self._clone_volume(snapshot['volume_name'],
+ snapshot['name'],
+ snapshot['volume_id'])
+
+ def delete_snapshot(self, snapshot):
+ """Deletes a snapshot."""
+ nfs_mount = self._get_provider_location(snapshot.volume_id)
+
+ if self._volume_not_present(nfs_mount, snapshot.name):
+ return True
+
+ self._execute('rm', self._get_volume_path(nfs_mount, snapshot.name),
+ run_as_root=True)
+
+ @staticmethod
+ def _check_dfm_flags():
+ """Raises error if any required configuration flag for OnCommand proxy
+ is missing."""
+ required_flags = ['netapp_wsdl_url',
+ 'netapp_login',
+ 'netapp_password',
+ 'netapp_server_hostname',
+ 'netapp_server_port']
+ for flag in required_flags:
+ if not getattr(FLAGS, flag, None):
+ raise exception.NovaException(_('%s is not set') % flag)
+
+ @staticmethod
+ def _get_client():
+ """Creates SOAP _client for ONTAP-7 DataFabric Service."""
+ client = suds.client.Client(FLAGS.netapp_wsdl_url,
+ username=FLAGS.netapp_login,
+ password=FLAGS.netapp_password)
+ soap_url = 'http://%s:%s/apis/soap/v1' % (
+ FLAGS.netapp_server_hostname,
+ FLAGS.netapp_server_port)
+ client.set_options(location=soap_url)
+
+ return client
+
+ def _get_volume_location(self, volume_id):
+ """Returns NFS mount address as <nfs_ip_address>:<nfs_mount_dir>"""
+ nfs_server_ip = self._get_host_ip(volume_id)
+ export_path = self._get_export_path(volume_id)
+ return (nfs_server_ip + ':' + export_path)
+
+ def _clone_volume(self, volume_name, clone_name, volume_id):
+ """Clones mounted volume with OnCommand proxy API"""
+ host_id = self._get_host_id(volume_id)
+ export_path = self._get_full_export_path(volume_id, host_id)
+
+ request = self._client.factory.create('Request')
+ request.Name = 'clone-start'
+
+ clone_start_args = ('<source-path>%s/%s</source-path>'
+ '<destination-path>%s/%s</destination-path>')
+
+ request.Args = text.Raw(clone_start_args % (export_path,
+ volume_name,
+ export_path,
+ clone_name))
+
+ resp = self._client.service.ApiProxy(Target=host_id,
+ Request=request)
+
+ if resp.Status == 'passed' and FLAGS.synchronous_snapshot_create:
+ clone_id = resp.Results['clone-id'][0]
+ clone_id_info = clone_id['clone-id-info'][0]
+ clone_operation_id = int(clone_id_info['clone-op-id'][0])
+
+ self._wait_for_clone_finished(clone_operation_id, host_id)
+ elif resp.Status == 'failed':
+ raise exception.NovaException(resp.Reason)
+
+ def _wait_for_clone_finished(self, clone_operation_id, host_id):
+ """
+ Polls ONTAP7 for clone status. Returns once clone is finished.
+ :param clone_operation_id: Identifier of ONTAP clone operation
+ """
+ clone_list_options = ('<clone-id>'
+ '<clone-id-info>'
+ '<clone-op-id>%d</clone-op-id>'
+ '<volume-uuid></volume-uuid>'
+ '</clone-id>'
+ '</clone-id-info>')
+
+ request = self._client.factory.create('Request')
+ request.Name = 'clone-list-status'
+ request.Args = text.Raw(clone_list_options % clone_operation_id)
+
+ resp = self._client.service.ApiProxy(Target=host_id, Request=request)
+
+ while resp.Status != 'passed':
+ time.sleep(1)
+ resp = self._client.service.ApiProxy(Target=host_id,
+ Request=request)
+
+ def _get_provider_location(self, volume_id):
+ """
+ Returns provider location for given volume
+ :param volume_id:
+ """
+ volume = self.db.volume_get(self._context, volume_id)
+ return volume.provider_location
+
+ def _get_host_ip(self, volume_id):
+ """Returns IP address for the given volume"""
+ return self._get_provider_location(volume_id).split(':')[0]
+
+ def _get_export_path(self, volume_id):
+ """Returns NFS export path for the given volume"""
+ return self._get_provider_location(volume_id).split(':')[1]
+
+ def _get_host_id(self, volume_id):
+ """Returns ID of the ONTAP-7 host"""
+ host_ip = self._get_host_ip(volume_id)
+ server = self._client.service
+
+ resp = server.HostListInfoIterStart(ObjectNameOrId=host_ip)
+ tag = resp.Tag
+
+ try:
+ res = server.HostListInfoIterNext(Tag=tag, Maximum=1)
+ if hasattr(res, 'Hosts') and res.Hosts.HostInfo:
+ return res.Hosts.HostInfo[0].HostId
+ finally:
+ server.HostListInfoIterEnd(Tag=tag)
+
+ def _get_full_export_path(self, volume_id, host_id):
+ """Returns full path to the NFS share, e.g. /vol/vol0/home"""
+ export_path = self._get_export_path(volume_id)
+ command_args = '<pathname>%s</pathname>'
+
+ request = self._client.factory.create('Request')
+ request.Name = 'nfs-exportfs-storage-path'
+ request.Args = text.Raw(command_args % export_path)
+
+ resp = self._client.service.ApiProxy(Target=host_id,
+ Request=request)
+
+ if resp.Status == 'passed':
+ return resp.Results['actual-pathname'][0]
+ elif resp.Status == 'failed':
+ raise exception.NovaException(resp.Reason)
+
+ def _volume_not_present(self, nfs_mount, volume_name):
+ """
+ Check if volume exists
+ """
+ try:
+ self._try_execute('ls', self._get_volume_path(nfs_mount,
+ volume_name))
+ except exception.ProcessExecutionError:
+ # If the volume isn't present
+ return True
+ return False
+
+ def _try_execute(self, *command, **kwargs):
+ # NOTE(vish): Volume commands can partially fail due to timing, but
+ # running them a second time on failure will usually
+ # recover nicely.
+ tries = 0
+ while True:
+ try:
+ self._execute(*command, **kwargs)
+ return True
+ except exception.ProcessExecutionError:
+ tries = tries + 1
+ if tries >= FLAGS.num_shell_tries:
+ raise
+ LOG.exception(_("Recovering from a failed execute. "
+ "Try number %s"), tries)
+ time.sleep(tries ** 2)
+
+ def _get_volume_path(self, nfs_share, volume_name):
+ """Get volume path (local fs path) for given volume name on given nfs
+ share
+ @param nfs_share string, example 172.18.194.100:/var/nfs
+ @param volume_name string,
+ example volume-91ee65ec-c473-4391-8c09-162b00c68a8c
+ """
+ return os.path.join(self._get_mount_point_for_share(nfs_share),
+ volume_name)
diff --git a/nova/volume/nfs.py b/nova/volume/nfs.py
new file mode 100644
index 000000000..f91b52018
--- /dev/null
+++ b/nova/volume/nfs.py
@@ -0,0 +1,293 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2012 NetApp, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import ctypes
+import errno
+import os
+
+from nova import exception
+from nova import flags
+from nova.openstack.common import cfg
+from nova.openstack.common import log as logging
+from nova.virt.libvirt import volume_nfs
+from nova.volume import driver
+
+LOG = logging.getLogger(__name__)
+
+volume_opts = [
+ cfg.StrOpt('nfs_shares_config',
+ default=None,
+ help='File with the list of available nfs shares'),
+ cfg.StrOpt('nfs_disk_util',
+ default='df',
+ help='Use du or df for free space calculation'),
+ cfg.BoolOpt('nfs_sparsed_volumes',
+ default=True,
+ help=('Create volumes as sparsed files which take no space.'
+ 'If set to False volume is created as regular file.'
+ 'In such case volume creation takes a lot of time.'))
+]
+
+FLAGS = flags.FLAGS
+FLAGS.register_opts(volume_opts)
+FLAGS.register_opts(volume_nfs.volume_opts)
+
+
+class NfsDriver(driver.VolumeDriver):
+ """NFS based volume driver. Creates file on NFS share for using it
+ as block device on hypervisor."""
+
+ def do_setup(self, context):
+ """Any initialization the volume driver does while starting"""
+ super(NfsDriver, self).do_setup(context)
+
+ config = FLAGS.nfs_shares_config
+ if not config:
+ LOG.warn(_("There's no NFS config file configured "))
+ if not config or not os.path.exists(config):
+ msg = _("NFS config file doesn't exist")
+ LOG.warn(msg)
+ raise exception.NfsException(msg)
+
+ try:
+ self._execute('mount.nfs', check_exit_code=False)
+ except OSError as exc:
+ if exc.errno == errno.ENOENT:
+ raise exception.NfsException('mount.nfs is not installed')
+ else:
+ raise
+
+ def check_for_setup_error(self):
+ """Just to override parent behavior"""
+ pass
+
+ def create_volume(self, volume):
+ """Creates a volume"""
+
+ self._ensure_shares_mounted()
+
+ volume['provider_location'] = self._find_share(volume['size'])
+
+ LOG.info(_('casted to %s') % volume['provider_location'])
+
+ self._do_create_volume(volume)
+
+ return {'provider_location': volume['provider_location']}
+
+ def delete_volume(self, volume):
+ """Deletes a logical volume."""
+
+ if not volume['provider_location']:
+ LOG.warn(_('Volume %s does not have provider_location specified, '
+ 'skipping'), volume['name'])
+ return
+
+ self._ensure_share_mounted(volume['provider_location'])
+
+ mounted_path = self.local_path(volume)
+
+ if not self._path_exists(mounted_path):
+ volume = volume['name']
+
+ LOG.warn(_('Trying to delete non-existing volume %(volume)s at '
+ 'path %(mounted_path)s') % locals())
+ return
+
+ self._execute('rm', '-f', mounted_path, run_as_root=True)
+
+ def ensure_export(self, ctx, volume):
+ """Synchronously recreates an export for a logical volume."""
+ self._ensure_share_mounted(volume['provider_location'])
+
+ def create_export(self, ctx, volume):
+ """Exports the volume. Can optionally return a Dictionary of changes
+ to the volume object to be persisted."""
+ pass
+
+ def remove_export(self, ctx, volume):
+ """Removes an export for a logical volume."""
+ pass
+
+ def check_for_export(self, context, volume_id):
+ """Make sure volume is exported."""
+ pass
+
+ def initialize_connection(self, volume, connector):
+ """Allow connection to connector and return connection info."""
+ data = {'export': volume['provider_location'],
+ 'name': volume['name']}
+ return {
+ 'driver_volume_type': 'nfs',
+ 'data': data
+ }
+
+ def terminate_connection(self, volume, connector):
+ """Disallow connection from connector"""
+ pass
+
+ def local_path(self, volume):
+ """Get volume path (mounted locally fs path) for given volume
+ :param volume: volume reference
+ """
+ nfs_share = volume['provider_location']
+ return os.path.join(self._get_mount_point_for_share(nfs_share),
+ volume['name'])
+
+ def _create_sparsed_file(self, path, size):
+ """Creates file with 0 disk usage"""
+ self._execute('truncate', '-s', self._sizestr(size),
+ path, run_as_root=True)
+
+ def _create_regular_file(self, path, size):
+ """Creates regular file of given size. Takes a lot of time for large
+ files"""
+ KB = 1024
+ MB = KB * 1024
+ GB = MB * 1024
+
+ block_size_mb = 1
+ block_count = size * GB / (block_size_mb * MB)
+
+ self._execute('dd', 'if=/dev/zero', 'of=%s' % path,
+ 'bs=%dM' % block_size_mb,
+ 'count=%d' % block_count,
+ run_as_root=True)
+
+ def _set_rw_permissions_for_all(self, path):
+ """Sets 666 permissions for the path"""
+ self._execute('chmod', 'ugo+rw', path, run_as_root=True)
+
+ def _do_create_volume(self, volume):
+ """Create a volume on given nfs_share
+ :param volume: volume reference
+ """
+ volume_path = self.local_path(volume)
+ volume_size = volume['size']
+
+ if FLAGS.nfs_sparsed_volumes:
+ self._create_sparsed_file(volume_path, volume_size)
+ else:
+ self._create_regular_file(volume_path, volume_size)
+
+ self._set_rw_permissions_for_all(volume_path)
+
+ def _ensure_shares_mounted(self):
+ """Look for NFS shares in the flags and tries to mount them locally"""
+ self._mounted_shares = []
+
+ for share in self._load_shares_config():
+ try:
+ self._ensure_share_mounted(share)
+ self._mounted_shares.append(share)
+ except Exception, exc:
+ LOG.warning('Exception during mounting %s' % (exc,))
+
+ LOG.debug('Available shares %s' % str(self._mounted_shares))
+
+ def _load_shares_config(self):
+ return [share.strip() for share in open(FLAGS.nfs_shares_config)
+ if share and not share.startswith('#')]
+
+ def _ensure_share_mounted(self, nfs_share):
+ """Mount NFS share
+ :param nfs_share:
+ """
+ mount_path = self._get_mount_point_for_share(nfs_share)
+ self._mount_nfs(nfs_share, mount_path, ensure=True)
+
+ def _find_share(self, volume_size_for):
+ """Choose NFS share among available ones for given volume size. Current
+ implementation looks for greatest capacity
+ :param volume_size_for: int size in Gb
+ """
+
+ if not self._mounted_shares:
+ raise exception.NfsNoSharesMounted()
+
+ greatest_size = 0
+ greatest_share = None
+
+ for nfs_share in self._mounted_shares:
+ capacity = self._get_available_capacity(nfs_share)
+ if capacity > greatest_size:
+ greatest_share = nfs_share
+ greatest_size = capacity
+
+ if volume_size_for * 1024 * 1024 * 1024 > greatest_size:
+ raise exception.NfsNoSuitableShareFound(
+ volume_size=volume_size_for)
+ return greatest_share
+
+ def _get_mount_point_for_share(self, nfs_share):
+ """
+ :param nfs_share: example 172.18.194.100:/var/nfs
+ """
+ return os.path.join(FLAGS.nfs_mount_point_base,
+ self._get_hash_str(nfs_share))
+
+ def _get_available_capacity(self, nfs_share):
+ """Calculate available space on the NFS share
+ :param nfs_share: example 172.18.194.100:/var/nfs
+ """
+ mount_point = self._get_mount_point_for_share(nfs_share)
+
+ out, _ = self._execute('df', '-P', '-B', '1', mount_point,
+ run_as_root=True)
+ out = out.splitlines()[1]
+
+ available = 0
+
+ if FLAGS.nfs_disk_util == 'df':
+ available = int(out.split()[3])
+ else:
+ size = int(out.split()[1])
+ out, _ = self._execute('du', '-sb', '--apparent-size',
+ '--exclude', '*snapshot*', mount_point,
+ run_as_root=True)
+ used = int(out.split()[0])
+ available = size - used
+
+ return available
+
+ def _mount_nfs(self, nfs_share, mount_path, ensure=False):
+ """Mount NFS share to mount path"""
+ if not self._path_exists(mount_path):
+ self._execute('mkdir', '-p', mount_path)
+
+ try:
+ self._execute('mount', '-t', 'nfs', nfs_share, mount_path,
+ run_as_root=True)
+ except exception.ProcessExecutionError as exc:
+ if ensure and 'already mounted' in exc.stderr:
+ LOG.warn(_("%s is already mounted"), nfs_share)
+ else:
+ raise
+
+ def _path_exists(self, path):
+ """Check given path """
+ try:
+ self._execute('stat', path, run_as_root=True)
+ return True
+ except exception.ProcessExecutionError as exc:
+ if 'No such file or directory' in exc.stderr:
+ return False
+ else:
+ raise
+
+ def _get_hash_str(self, base_str):
+ """returns string that represents hash of base_str (in a hex format)"""
+ return str(ctypes.c_uint64(hash(base_str)).value)