diff options
| author | Ben Swartzlander <bswartz@netapp.com> | 2012-09-01 23:39:39 -0400 |
|---|---|---|
| committer | Ben Swartzlander <bswartz@netapp.com> | 2012-09-12 12:43:44 -0400 |
| commit | 772c5d47d5bdffcd4ff8e09f4116d22568bf6eb9 (patch) | |
| tree | c2a11bc6bb2dbe9e78e1f6f603e86042a5bb5087 /nova/volume | |
| parent | 76d094eeba1bcbba16d24e40aea24bb7729b4a30 (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.py | 294 | ||||
| -rw-r--r-- | nova/volume/netapp_nfs.py | 267 | ||||
| -rw-r--r-- | nova/volume/nfs.py | 293 |
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) |
