VMAX manila plugin - Support for VMAX in Manila

VMAX plugin is the plugin which manages the VMAX to
provide shared filesystems using the Dell EMC manila
driver framework.

DocImpact
Change-Id: I0b69f013443217f2053bbbfdec36dff226664b34
Implements: blueprint vmax-manila-support
This commit is contained in:
xing-yang 2016-11-14 09:52:01 -05:00 committed by Helen Walsh
parent dc43f741f8
commit 1b0042f052
20 changed files with 10401 additions and 1 deletions

View File

@ -41,6 +41,8 @@ Mapping of share drivers and share features support
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| NetApp Clustered Data ONTAP | J | L | L | L | J | J | N | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| EMC VMAX | O | \- | O | \- | O | O | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| EMC VNX | J | \- | \- | \- | J | J | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| EMC Unity | N | \- | N | \- | N | N | \- | \- |
@ -99,6 +101,8 @@ Mapping of share drivers and share access rules support
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
| NetApp Clustered Data ONTAP | NFS (J) | CIFS (J) | \- | \- | NFS (K) | CIFS (M) | \- | \- |
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
| EMC VMAX | NFS (O) | CIFS (O) | \- | \- | NFS (O) | CIFS (O) | \- | \- |
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
| EMC VNX | NFS (J) | CIFS (J) | \- | \- | NFS (L) | CIFS (L) | \- | \- |
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
| EMC Unity | NFS (N) | CIFS (N) | \- | \- | NFS (N) | CIFS (N) | \- | \- |
@ -154,6 +158,8 @@ Mapping of share drivers and security services support
+----------------------------------------+------------------+-----------------+------------------+
| NetApp Clustered Data ONTAP | J | J | J |
+----------------------------------------+------------------+-----------------+------------------+
| EMC VMAX | O | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
| EMC VNX | J | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
| EMC Unity | N | \- | \- |
@ -212,6 +218,8 @@ More information: :ref:`capabilities_and_extra_specs`
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| NetApp Clustered Data ONTAP | J | K | M | M | M | L | \- | J | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| EMC VMAX | O | \- | \- | \- | \- | O | \- | O | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| EMC VNX | J | \- | \- | \- | \- | L | \- | J | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| EMC Unity | N | \- | \- | \- | N | \- | \- | N | \- |

View File

@ -650,6 +650,18 @@ class VserverNotSpecified(NetAppException):
message = _("Vserver not specified.")
class EMCVmaxXMLAPIError(Invalid):
message = _("%(err)s")
class EMCVmaxLockRequiredException(ManilaException):
message = _("Unable to acquire lock(s).")
class EMCVmaxInvalidMoverID(ManilaException):
message = _("Invalid mover or vdm %(id)s.")
class EMCVnxXMLAPIError(Invalid):
message = _("%(err)s")

View File

@ -54,6 +54,7 @@ import manila.share.drivers.container.driver
import manila.share.drivers.container.storage_helper
import manila.share.drivers.dell_emc.driver
import manila.share.drivers.dell_emc.plugins.isilon.isilon
import manila.share.drivers.dell_emc.plugins.vmax.connection
import manila.share.drivers.generic
import manila.share.drivers.glusterfs
import manila.share.drivers.glusterfs.common
@ -123,6 +124,7 @@ _global_opt_lists = [
manila.share.drivers.container.driver.container_opts,
manila.share.drivers.container.storage_helper.lv_opts,
manila.share.drivers.dell_emc.driver.EMC_NAS_OPTS,
manila.share.drivers.dell_emc.plugins.vmax.connection.VMAX_OPTS,
manila.share.drivers.generic.share_opts,
manila.share.drivers.glusterfs.common.glusterfs_common_opts,
manila.share.drivers.glusterfs.GlusterfsManilaShare_opts,

View File

@ -40,7 +40,7 @@ EMC_NAS_OPTS = [
help='Use secure connection to server.'),
cfg.StrOpt('emc_share_backend',
ignore_case=True,
choices=['isilon', 'vnx', 'unity'],
choices=['isilon', 'vnx', 'unity', 'vmax'],
help='Share backend.'),
cfg.StrOpt('emc_nas_root_dir',
help='The root directory where shares will be located.')

View File

@ -0,0 +1,880 @@
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
# 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.
"""VMAX backend for the EMC Manila driver."""
import copy
import random
from oslo_config import cfg
from oslo_log import log
from oslo_utils import excutils
from oslo_utils import units
from manila.common import constants as const
from manila import exception
from manila.i18n import _, _LE, _LI, _LW
from manila.share.drivers.dell_emc.plugins import base as driver
from manila.share.drivers.dell_emc.plugins.vmax import (
object_manager as manager)
from manila.share.drivers.dell_emc.plugins.vmax import constants
from manila.share.drivers.dell_emc.plugins.vmax import utils as vmax_utils
from manila.share import utils as share_utils
from manila import utils
VERSION = "1.0.0"
LOG = log.getLogger(__name__)
VMAX_OPTS = [
cfg.StrOpt('vmax_server_container',
help='Data mover to host the NAS server.'),
cfg.ListOpt('vmax_share_data_pools',
help='Comma separated list of pools that can be used to '
'persist share data.'),
cfg.ListOpt('vmax_ethernet_ports',
help='Comma separated list of ports that can be used for '
'share server interfaces. Members of the list '
'can be Unix-style glob expressions.')
]
CONF = cfg.CONF
CONF.register_opts(VMAX_OPTS)
@vmax_utils.decorate_all_methods(vmax_utils.log_enter_exit,
debug_only=True)
class VMAXStorageConnection(driver.StorageConnection):
"""Implements vmax specific functionality for EMC Manila driver."""
@vmax_utils.log_enter_exit
def __init__(self, *args, **kwargs):
super(VMAXStorageConnection, self).__init__(*args, **kwargs)
if 'configuration' in kwargs:
kwargs['configuration'].append_config_values(VMAX_OPTS)
self.mover_name = None
self.pools = None
self.manager = None
self.pool_conf = None
self.reserved_percentage = None
self.driver_handles_share_servers = True
self.port_conf = None
def create_share(self, context, share, share_server=None):
"""Create a share and export it based on protocol used."""
share_name = share['id']
size = share['size'] * units.Ki
share_proto = share['share_proto'].upper()
# Validate the share protocol
if share_proto not in ('NFS', 'CIFS'):
raise exception.InvalidShare(
reason=(_('Invalid NAS protocol supplied: %s.')
% share_proto))
# Get the pool name from share host field
pool_name = share_utils.extract_host(share['host'], level='pool')
if not pool_name:
message = (_("Pool is not available in the share host %s.") %
share['host'])
raise exception.InvalidHost(reason=message)
# Validate share server
self._share_server_validation(share_server)
if share_proto == 'CIFS':
vdm_name = self._get_share_server_name(share_server)
server_name = vdm_name
# Check if CIFS server exists.
status, server = self._get_context('CIFSServer').get(server_name,
vdm_name)
if status != constants.STATUS_OK:
message = (_("CIFS server %s not found.") % server_name)
LOG.error(message)
raise exception.EMCVmaxXMLAPIError(err=message)
self._allocate_container(share_name, size, share_server, pool_name)
if share_proto == 'NFS':
location = self._create_nfs_share(share_name, share_server)
elif share_proto == 'CIFS':
location = self._create_cifs_share(share_name, share_server)
return location
def _share_server_validation(self, share_server):
"""Validate the share server."""
if not share_server:
msg = _('Share server not provided')
raise exception.InvalidInput(reason=msg)
backend_details = share_server.get('backend_details')
vdm = backend_details.get(
'share_server_name') if backend_details else None
if vdm is None:
message = _("No share server found.")
LOG.error(message)
raise exception.EMCVmaxXMLAPIError(err=message)
def _allocate_container(self, share_name, size, share_server, pool_name):
"""Allocate file system for share."""
vdm_name = self._get_share_server_name(share_server)
self._get_context('FileSystem').create(
share_name, size, pool_name, vdm_name)
def _allocate_container_from_snapshot(self, share, snapshot, share_server,
pool_name):
"""Allocate file system from snapshot."""
vdm_name = self._get_share_server_name(share_server)
interconn_id = self._get_context('Mover').get_interconnect_id(
self.mover_name, self.mover_name)
self._get_context('FileSystem').create_from_snapshot(
share['id'], snapshot['id'], snapshot['share_id'],
pool_name, vdm_name, interconn_id)
nwe_size = share['size'] * units.Ki
self._get_context('FileSystem').extend(share['id'], pool_name,
nwe_size)
@vmax_utils.log_enter_exit
def _create_cifs_share(self, share_name, share_server):
"""Create CIFS share."""
vdm_name = self._get_share_server_name(share_server)
server_name = vdm_name
# Get available CIFS Server and interface (one CIFS server per VDM)
status, server = self._get_context('CIFSServer').get(server_name,
vdm_name)
if 'interfaces' not in server or len(server['interfaces']) == 0:
message = (_("CIFS server %s doesn't have interface, "
"so the share is inaccessible.")
% server['compName'])
LOG.error(message)
raise exception.EMCVmaxXMLAPIError(err=message)
interface = server['interfaces'][0]
self._get_context('CIFSShare').create(share_name, server['name'],
vdm_name)
self._get_context('CIFSShare').disable_share_access(share_name,
vdm_name)
locations = []
location = (r'\\%(interface)s\%(name)s' %
{'interface': interface, 'name': share_name})
locations.append(location)
return locations
@vmax_utils.log_enter_exit
def _create_nfs_share(self, share_name, share_server):
"""Create NFS share."""
vdm_name = self._get_share_server_name(share_server)
self._get_context('NFSShare').create(share_name, vdm_name)
return ('%(nfs_if)s:/%(share_name)s'
% {'nfs_if': share_server['backend_details']['nfs_if'],
'share_name': share_name})
def create_share_from_snapshot(self, context, share, snapshot,
share_server=None):
"""Create a share from a snapshot - clone a snapshot."""
share_name = share['id']
share_proto = share['share_proto'].upper()
# Validate the share protocol
if share_proto not in ('NFS', 'CIFS'):
raise exception.InvalidShare(
reason=(_('Invalid NAS protocol supplied: %s.')
% share_proto))
# Get the pool name from share host field
pool_name = share_utils.extract_host(share['host'], level='pool')
if not pool_name:
message = (_("Pool is not available in the share host %s.") %
share['host'])
raise exception.InvalidHost(reason=message)
self._share_server_validation(share_server)
self._allocate_container_from_snapshot(
share, snapshot, share_server, pool_name)
if share_proto == 'NFS':
self._create_nfs_share(share_name, share_server)
location = ('%(nfs_if)s:/%(share_name)s'
% {'nfs_if': share_server['backend_details']['nfs_if'],
'share_name': share_name})
elif share_proto == 'CIFS':
location = self._create_cifs_share(share_name, share_server)
return location
def create_snapshot(self, context, snapshot, share_server=None):
"""Create snapshot from share."""
share_name = snapshot['share_id']
status, filesystem = self._get_context('FileSystem').get(share_name)
if status != constants.STATUS_OK:
message = (_("File System %s not found.") % share_name)
LOG.error(message)
raise exception.EMCVmaxXMLAPIError(err=message)
pool_id = filesystem['pools_id'][0]
self._get_context('Snapshot').create(snapshot['id'],
snapshot['share_id'],
pool_id)
def delete_share(self, context, share, share_server=None):
"""Delete a share."""
if share_server is None:
LOG.warning(_LW("Share network should be specified for "
"share deletion."))
return
share_proto = share['share_proto'].upper()
if share_proto == 'NFS':
self._delete_nfs_share(share, share_server)
elif share_proto == 'CIFS':
self._delete_cifs_share(share, share_server)
else:
raise exception.InvalidShare(
reason=_('Unsupported share protocol'))
@vmax_utils.log_enter_exit
def _delete_cifs_share(self, share, share_server):
"""Delete CIFS share."""
vdm_name = self._get_share_server_name(share_server)
name = share['id']
self._get_context('CIFSShare').delete(name, vdm_name)
self._deallocate_container(name, vdm_name)
@vmax_utils.log_enter_exit
def _delete_nfs_share(self, share, share_server):
"""Delete NFS share."""
vdm_name = self._get_share_server_name(share_server)
name = share['id']
self._get_context('NFSShare').delete(name, vdm_name)
self._deallocate_container(name, vdm_name)
@vmax_utils.log_enter_exit
def _deallocate_container(self, share_name, vdm_name):
"""Delete underneath objects of the share."""
path = '/' + share_name
try:
# Delete mount point
self._get_context('MountPoint').delete(path, vdm_name)
except exception.EMCVmaxXMLAPIError as e:
LOG.exception(_LE("CIFS server %(name)s on mover %(mover_name)s "
"not found due to error %(err)s. Skip the "
"deletion."),
{'name': path, 'mover_name': vdm_name,
'err': e.message})
try:
# Delete file system
self._get_context('FileSystem').delete(share_name)
except exception.EMCVmaxXMLAPIError as e:
LOG.exception(_LE("File system %(share_name)s not found due to"
"error %(err)s. Skip the deletion."),
{'share_name': share_name,
'err': e.message})
def delete_snapshot(self, context, snapshot, share_server=None):
"""Delete a snapshot."""
self._get_context('Snapshot').delete(snapshot['id'])
def ensure_share(self, context, share, share_server=None):
"""Ensure that the share is exported."""
def extend_share(self, share, new_size, share_server=None):
# Get the pool name from share host field
pool_name = share_utils.extract_host(share['host'], level='pool')
if not pool_name:
message = (_("Pool is not available in the share host %s.") %
share['host'])
raise exception.InvalidHost(reason=message)
share_name = share['id']
self._get_context('FileSystem').extend(
share_name, pool_name, new_size * units.Ki)
def allow_access(self, context, share, access, share_server=None):
"""Allow access to a share."""
access_level = access['access_level']
if access_level not in const.ACCESS_LEVELS:
raise exception.InvalidShareAccessLevel(level=access_level)
share_proto = share['share_proto']
if share_proto == 'NFS':
self._nfs_allow_access(context, share, access, share_server)
elif share_proto == 'CIFS':
self._cifs_allow_access(context, share, access, share_server)
else:
raise exception.InvalidShare(
reason=(_('Invalid NAS protocol supplied: %s.')
% share_proto))
@vmax_utils.log_enter_exit
def _cifs_allow_access(self, context, share, access, share_server):
"""Allow access to CIFS share."""
vdm_name = self._get_share_server_name(share_server)
share_name = share['id']
if access['access_type'] != 'user':
reason = _('Only user access type allowed for CIFS share')
raise exception.InvalidShareAccess(reason=reason)
user_name = access['access_to']
access_level = access['access_level']
if access_level == const.ACCESS_LEVEL_RW:
cifs_access = constants.CIFS_ACL_FULLCONTROL
else:
cifs_access = constants.CIFS_ACL_READ
# Check if CIFS server exists.
server_name = vdm_name
status, server = self._get_context('CIFSServer').get(server_name,
vdm_name)
if status != constants.STATUS_OK:
message = (_("CIFS server %s not found.") % server_name)
LOG.error(message)
raise exception.EMCVmaxXMLAPIError(err=message)
self._get_context('CIFSShare').allow_share_access(
vdm_name,
share_name,
user_name,
server['domain'],
access=cifs_access)
@vmax_utils.log_enter_exit
def _nfs_allow_access(self, context, share, access, share_server):
"""Allow access to NFS share."""
vdm_name = self._get_share_server_name(share_server)
access_type = access['access_type']
if access_type != 'ip':
reason = _('Only ip access type allowed.')
raise exception.InvalidShareAccess(reason=reason)
host_ip = access['access_to']
access_level = access['access_level']
self._get_context('NFSShare').allow_share_access(
share['id'], host_ip, vdm_name, access_level)
def update_access(self, context, share, access_rules, add_rules,
delete_rules, share_server=None):
# deleting rules
for rule in delete_rules:
self.deny_access(context, share, rule, share_server)
# adding rules
for rule in add_rules:
self.allow_access(context, share, rule, share_server)
# recovery mode
if not (add_rules or delete_rules):
white_list = []
for rule in access_rules:
self.allow_access(context, share, rule, share_server)
white_list.append(rule['access_to'])
self.clear_access(share, share_server, white_list)
def clear_access(self, share, share_server, white_list):
share_proto = share['share_proto'].upper()
share_name = share['id']
if share_proto == 'CIFS':
self._cifs_clear_access(share_name, share_server, white_list)
elif share_proto == 'NFS':
self._nfs_clear_access(share_name, share_server, white_list)
@vmax_utils.log_enter_exit
def _cifs_clear_access(self, share_name, share_server, white_list):
"""Clear access for CIFS share except hosts in the white list."""
vdm_name = self._get_share_server_name(share_server)
# Check if CIFS server exists.
server_name = vdm_name
status, server = self._get_context('CIFSServer').get(server_name,
vdm_name)
if status != constants.STATUS_OK:
message = (_("CIFS server %(server_name)s has issue. "
"Detail: %(status)s") %
{'server_name': server_name, 'status': status})
raise exception.EMCVmaxXMLAPIError(err=message)
self._get_context('CIFSShare').clear_share_access(
share_name=share_name,
mover_name=vdm_name,
domain=server['domain'],
white_list_users=white_list)
@vmax_utils.log_enter_exit
def _nfs_clear_access(self, share_name, share_server, white_list):
"""Clear access for NFS share except hosts in the white list."""
self._get_context('NFSShare').clear_share_access(
share_name=share_name,
mover_name=self._get_share_server_name(share_server),
white_list_hosts=white_list)
def deny_access(self, context, share, access, share_server=None):
"""Deny access to a share."""
share_proto = share['share_proto']
if share_proto == 'NFS':
self._nfs_deny_access(share, access, share_server)
elif share_proto == 'CIFS':
self._cifs_deny_access(share, access, share_server)
else:
raise exception.InvalidShare(
reason=_('Unsupported share protocol'))
@vmax_utils.log_enter_exit
def _cifs_deny_access(self, share, access, share_server):
"""Deny access to CIFS share."""
vdm_name = self._get_share_server_name(share_server)
share_name = share['id']
if access['access_type'] != 'user':
LOG.warning(_LW("Only user access type allowed for CIFS share."))
return
user_name = access['access_to']
access_level = access['access_level']
if access_level == const.ACCESS_LEVEL_RW:
cifs_access = constants.CIFS_ACL_FULLCONTROL
else:
cifs_access = constants.CIFS_ACL_READ
# Check if CIFS server exists.
server_name = vdm_name
status, server = self._get_context('CIFSServer').get(server_name,
vdm_name)
if status != constants.STATUS_OK:
message = (_("CIFS server %s not found.") % server_name)
LOG.error(message)
raise exception.EMCVmaxXMLAPIError(err=message)
self._get_context('CIFSShare').deny_share_access(
vdm_name,
share_name,
user_name,
server['domain'],
access=cifs_access)
@vmax_utils.log_enter_exit
def _nfs_deny_access(self, share, access, share_server):
"""Deny access to NFS share."""
vdm_name = self._get_share_server_name(share_server)
access_type = access['access_type']
if access_type != 'ip':
LOG.warning(_LW("Only ip access type allowed."))
return
host_ip = access['access_to']
self._get_context('NFSShare').deny_share_access(share['id'], host_ip,
vdm_name)
def check_for_setup_error(self):
"""Check for setup error."""
# To verify the input from Manila configuration
status, out = self._get_context('Mover').get_ref(self.mover_name,
True)
if constants.STATUS_ERROR == status:
message = (_("Could not find Data Mover by name: %s.") %
self.mover_name)
LOG.error(message)
raise exception.InvalidParameterValue(err=message)
self.pools = self._get_managed_storage_pools(self.pool_conf)
def _get_managed_storage_pools(self, pools):
matched_pools = set()
if pools:
# Get the real pools from the backend storage
status, backend_pools = self._get_context('StoragePool').get_all()
if status != constants.STATUS_OK:
message = (_("Failed to get storage pool information. "
"Reason: %s") % backend_pools)
LOG.error(message)
raise exception.EMCVmaxXMLAPIError(err=message)
real_pools = set([item for item in backend_pools])
conf_pools = set([item.strip() for item in pools])
matched_pools, unmatched_pools = vmax_utils.do_match_any(
real_pools, conf_pools)
if not matched_pools:
msg = (_("None of the specified storage pools to be managed "
"exist. Please check your configuration "
"emc_nas_pool_names in manila.conf. "
"The available pools in the backend are %s.") %
",".join(real_pools))
raise exception.InvalidParameterValue(err=msg)
LOG.info(_LI("Storage pools: %s will be managed."),
",".join(matched_pools))
else:
LOG.debug("No storage pool is specified, so all pools "
"in storage system will be managed.")
return matched_pools
def connect(self, emc_share_driver, context):
"""Connect to VMAX NAS server."""
config = emc_share_driver.configuration
config.append_config_values(VMAX_OPTS)
self.mover_name = config.vmax_server_container
self.pool_conf = config.safe_get('vmax_share_data_pools')
self.reserved_percentage = config.safe_get('reserved_share_percentage')
if self.reserved_percentage is None:
self.reserved_percentage = 0
self.manager = manager.StorageObjectManager(config)
self.port_conf = config.safe_get('emc_interface_ports')
def get_managed_ports(self):
# Get the real ports(devices) list from the backend storage
real_ports = self._get_physical_devices(self.mover_name)
if not self.port_conf:
LOG.debug("No ports are specified, so any of the ports on the "
"Data Mover can be used.")
return real_ports
matched_ports, unmanaged_ports = vmax_utils.do_match_any(
real_ports, self.port_conf)
if not matched_ports:
msg = (_("None of the specified network ports exist. "
"Please check your configuration emc_interface_ports "
"in manila.conf. The available ports on the Data Mover "
"are %s.") %
",".join(real_ports))
raise exception.BadConfigurationException(reason=msg)
LOG.debug("Ports: %s can be used.", ",".join(matched_ports))
return list(matched_ports)
def update_share_stats(self, stats_dict):
"""Communicate with EMCNASClient to get the stats."""
stats_dict['driver_version'] = VERSION
self._get_context('Mover').get_ref(self.mover_name, True)
stats_dict['pools'] = []
status, pools = self._get_context('StoragePool').get_all()
for name, pool in pools.items():
if not self.pools or pool['name'] in self.pools:
total_size = float(pool['total_size'])
used_size = float(pool['used_size'])
pool_stat = {
'pool_name': pool['name'],
'total_capacity_gb': total_size,
'free_capacity_gb': total_size - used_size,
'qos': False,
'reserved_percentage': self.reserved_percentage,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
}
stats_dict['pools'].append(pool_stat)
if not stats_dict['pools']:
message = _("Failed to update storage pool.")
LOG.error(message)
raise exception.EMCVmaxXMLAPIError(err=message)
def get_pool(self, share):
"""Get the pool name of the share."""
share_name = share['id']
status, filesystem = self._get_context('FileSystem').get(share_name)
if status != constants.STATUS_OK:
message = (_("File System %(name)s not found. "
"Reason: %(err)s") %
{'name': share_name, 'err': filesystem})
LOG.error(message)
raise exception.EMCVmaxXMLAPIError(err=message)
pool_id = filesystem['pools_id'][0]
# Get the real pools from the backend storage
status, backend_pools = self._get_context('StoragePool').get_all()
if status != constants.STATUS_OK:
message = (_("Failed to get storage pool information. "
"Reason: %s") % backend_pools)
LOG.error(message)
raise exception.EMCVmaxXMLAPIError(err=message)
for name, pool_info in backend_pools.items():
if pool_info['id'] == pool_id:
return name
available_pools = [item for item in backend_pools]
message = (_("No matched pool name for share: %(share)s. "
"Available pools: %(pools)s") %
{'share': share_name, 'pools': available_pools})
raise exception.EMCVmaxXMLAPIError(err=message)
def get_network_allocations_number(self):
"""Returns number of network allocations for creating VIFs."""
return constants.IP_ALLOCATIONS
def setup_server(self, network_info, metadata=None):
"""Set up and configure share server.
Sets up and configures share server with given network parameters.
"""
# Only support single security service with type 'active_directory'
vdm_name = network_info['server_id']
vlan_id = network_info['segmentation_id']
active_directory = None
allocated_interfaces = []
if network_info.get('security_services'):
is_valid, active_directory = self._get_valid_security_service(
network_info['security_services'])
if not is_valid:
raise exception.EMCVmaxXMLAPIError(err=active_directory)
try:
if not self._vdm_exist(vdm_name):
LOG.debug('Share server %s not found, creating '
'share server...', vdm_name)
self._get_context('VDM').create(vdm_name, self.mover_name)
netmask = utils.cidr_to_netmask(network_info['cidr'])
devices = self.get_managed_ports()
for net_info in network_info['network_allocations']:
random.shuffle(devices)
interface = {
'name': net_info['id'][-12:],
'device_name': devices[0],
'ip': net_info['ip_address'],
'mover_name': self.mover_name,
'net_mask': netmask,
'vlan_id': vlan_id if vlan_id else -1,
}
self._get_context('MoverInterface').create(interface)
allocated_interfaces.append(interface)
cifs_interface = allocated_interfaces[0]
nfs_interface = allocated_interfaces[1]
if active_directory:
self._configure_active_directory(
active_directory, vdm_name, cifs_interface)
self._get_context('VDM').attach_nfs_interface(
vdm_name, nfs_interface['name'])
return {
'share_server_name': vdm_name,
'cifs_if': cifs_interface['ip'],
'nfs_if': nfs_interface['ip'],
}
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Could not setup server'))
server_details = self._construct_backend_details(
vdm_name, allocated_interfaces)
self.teardown_server(
server_details, network_info['security_services'])
def _construct_backend_details(self, vdm_name, interfaces):
if_number = len(interfaces)
cifs_if = interfaces[0]['ip'] if if_number > 0 else None
nfs_if = interfaces[1]['ip'] if if_number > 1 else None
return {
'share_server_name': vdm_name,
'cifs_if': cifs_if,
'nfs_if': nfs_if,
}
@vmax_utils.log_enter_exit
def _vdm_exist(self, name):
status, out = self._get_context('VDM').get(name)
if constants.STATUS_OK != status:
return False
return True
def _get_physical_devices(self, mover_name):
"""Get a proper network device to create interface."""
devices = self._get_context('Mover').get_physical_devices(mover_name)
if not devices:
message = (_("Could not get physical device port on mover %s.") %
self.mover_name)
LOG.error(message)
raise exception.EMCVmaxXMLAPIError(err=message)
return devices
def _configure_active_directory(
self, security_service, vdm_name, interface):
domain = security_service['domain']
server = security_service['dns_ip']
self._get_context('DNSDomain').create(self.mover_name, domain, server)
cifs_server_args = {
'name': vdm_name,
'interface_ip': interface['ip'],
'domain_name': security_service['domain'],
'user_name': security_service['user'],
'password': security_service['password'],
'mover_name': vdm_name,
'is_vdm': True,
}
self._get_context('CIFSServer').create(cifs_server_args)
def teardown_server(self, server_details, security_services=None):
"""Teardown share server."""
if not server_details:
LOG.debug('Server details are empty.')
return
vdm_name = server_details.get('share_server_name')
if not vdm_name:
LOG.debug('No share server found in server details.')
return
cifs_if = server_details.get('cifs_if')
nfs_if = server_details.get('nfs_if')
status, vdm = self._get_context('VDM').get(vdm_name)
if constants.STATUS_OK != status:
LOG.debug('Share server %s not found.', vdm_name)
return
interfaces = self._get_context('VDM').get_interfaces(vdm_name)
for if_name in interfaces['nfs']:
self._get_context('VDM').detach_nfs_interface(vdm_name, if_name)
if security_services:
# Only support single security service with type 'active_directory'
is_valid, active_directory = self._get_valid_security_service(
security_services)
if is_valid:
status, servers = self._get_context('CIFSServer').get_all(
vdm_name)
if constants.STATUS_OK != status:
LOG.error(_LE('Could not find CIFS server by name: %s.'),
vdm_name)
else:
cifs_servers = copy.deepcopy(servers)
for name, server in cifs_servers.items():
# Unjoin CIFS Server from domain
cifs_server_args = {
'name': server['name'],
'join_domain': False,
'user_name': active_directory['user'],
'password': active_directory['password'],
'mover_name': vdm_name,
'is_vdm': True,
}
try:
self._get_context('CIFSServer').modify(
cifs_server_args)
except exception.EMCVmaxXMLAPIError as expt:
LOG.debug("Failed to modify CIFS server "
"%(server)s. Reason: %(err)s.",
{'server': server, 'err': expt})
self._get_context('CIFSServer').delete(name, vdm_name)
# Delete interface from Data Mover
if cifs_if:
self._get_context('MoverInterface').delete(cifs_if,
self.mover_name)
if nfs_if:
self._get_context('MoverInterface').delete(nfs_if,
self.mover_name)
# Delete Virtual Data Mover
self._get_context('VDM').delete(vdm_name)
def _get_valid_security_service(self, security_services):
"""Validate security services and return a supported security service.
:param security_services:
:returns: (<is_valid>, <data>) -- <is_valid> is true to indicate
security_services includes zero or single security service for
active directory. Otherwise, it would return false. <data> return
error message when <is_valid> is false. Otherwise, it will
return zero or single security service for active directory.
"""
# Only support single security service with type 'active_directory'
if (len(security_services) > 1 or
(security_services and
security_services[0]['type'] != 'active_directory')):
return False, _("Unsupported security services. "
"Only support single security service and "
"only support type 'active_directory'")
return True, security_services[0]
def _get_share_server_name(self, share_server):
try:
return share_server['backend_details']['share_server_name']
except Exception:
LOG.debug("Didn't get share server name from share_server %s.",
share_server)
return share_server['id']
def _get_context(self, context_type):
return self.manager.getStorageContext(context_type)

View File

@ -0,0 +1,167 @@
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
# 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 pipes
from oslo_concurrency import processutils
from oslo_log import log
from oslo_utils import excutils
import six
from six.moves import http_cookiejar
from six.moves.urllib import error as url_error # pylint: disable=E0611
from six.moves.urllib import request as url_request # pylint: disable=E0611
from manila import exception
from manila.i18n import _
from manila.i18n import _LE
from manila.share.drivers.dell_emc.plugins.vmax import constants
from manila import utils
LOG = log.getLogger(__name__)
class XMLAPIConnector(object):
def __init__(self, configuration, debug=True):
super(XMLAPIConnector, self).__init__()
self.storage_ip = configuration.emc_nas_server
self.username = configuration.emc_nas_login
self.password = configuration.emc_nas_password
self.debug = debug
self.auth_url = 'https://' + self.storage_ip + '/Login'
self._url = ('https://' + self.storage_ip
+ '/servlets/CelerraManagementServices')
https_handler = url_request.HTTPSHandler()
cookie_handler = url_request.HTTPCookieProcessor(
http_cookiejar.CookieJar())
self.url_opener = url_request.build_opener(https_handler,
cookie_handler)
self._do_setup()
def _do_setup(self):
credential = ('user=' + self.username
+ '&password=' + self.password
+ '&Login=Login')
req = url_request.Request(self.auth_url, credential,
constants.CONTENT_TYPE_URLENCODE)
resp = self.url_opener.open(req)
resp_body = resp.read()
self._http_log_resp(resp, resp_body)
def _http_log_req(self, req):
if not self.debug:
return
string_parts = ['curl -i']
string_parts.append(' -X %s' % req.get_method())
for k in req.headers:
header = ' -H "%s: %s"' % (k, req.headers[k])
string_parts.append(header)
if req.data:
string_parts.append(" -d '%s'" % req.data)
string_parts.append(' ' + req.get_full_url())
LOG.debug("\nREQ: %s.\n", "".join(string_parts))
def _http_log_resp(self, resp, body):
if not self.debug:
return
headers = six.text_type(resp.headers).replace('\n', '\\n')
LOG.debug(
'RESP: [%(code)s] %(resp_hdrs)s\n'
'RESP BODY: %(resp_b)s.\n',
{
'code': resp.getcode(),
'resp_hdrs': headers,
'resp_b': body,
}
)
def _request(self, req_body=None, method=None,
header=constants.CONTENT_TYPE_URLENCODE):
req = url_request.Request(self._url, req_body, header)
if method not in (None, 'GET', 'POST'):
req.get_method = lambda: method
self._http_log_req(req)
try:
resp = self.url_opener.open(req)
resp_body = resp.read()
self._http_log_resp(resp, resp_body)
except url_error.HTTPError as http_err:
if '403' == six.text_type(http_err.code):
raise exception.NotAuthorized()
else:
err = {'errorCode': -1,
'httpStatusCode': http_err.code,
'messages': six.text_type(http_err),
'request': req_body}
msg = (_("The request is invalid. Reason: %(reason)s") %
{'reason': err})
raise exception.ManilaException(message=msg)
return resp_body
def request(self, req_body=None, method=None,
header=constants.CONTENT_TYPE_URLENCODE):
try:
resp_body = self._request(req_body, method, header)
except exception.NotAuthorized:
LOG.debug("Login again because client certification "
"may be expired.")
self._do_setup()
resp_body = self._request(req_body, method, header)
return resp_body
class SSHConnector(object):
def __init__(self, configuration, debug=True):
super(SSHConnector, self).__init__()
self.storage_ip = configuration.emc_nas_server
self.username = configuration.emc_nas_login
self.password = configuration.emc_nas_password
self.debug = debug
self.sshpool = utils.SSHPool(ip=self.storage_ip,
port=22,
conn_timeout=None,
login=self.username,
password=self.password)
def run_ssh(self, cmd_list, check_exit_code=False):
command = ' '.join(pipes.quote(cmd_arg) for cmd_arg in cmd_list)
with self.sshpool.item() as ssh:
try:
out, err = processutils.ssh_execute(
ssh, command, check_exit_code=check_exit_code)
self.log_request(command, out, err)
return out, err
except processutils.ProcessExecutionError as e:
with excutils.save_and_reraise_exception():
LOG.error(_LE('Error running SSH command: %(cmd)s. '
'Error: %(excmsg)s.'),
{'cmd': command, 'excmsg': e})
def log_request(self, cmd, out, err):
if not self.debug:
return
LOG.debug("\nSSH command: %s.\n", cmd)
LOG.debug("SSH command output: out=%(out)s, err=%(err)s.\n",
{'out': out, 'err': err})

View File

@ -0,0 +1,55 @@
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
# 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.
STATUS_OK = 'ok'
STATUS_INFO = 'info'
STATUS_DEBUG = 'debug'
STATUS_WARNING = 'warning'
STATUS_ERROR = 'error'
STATUS_NOT_FOUND = 'not_found'
MSG_GENERAL_ERROR = '13690601492'
MSG_INVALID_VDM_ID = '14227341325'
MSG_INVALID_MOVER_ID = '14227341323'
MSG_FILESYSTEM_NOT_FOUND = "18522112101"
MSG_FILESYSTEM_EXIST = '13691191325'
MSG_VDM_EXIST = '13421840550'
MSG_SNAP_EXIST = '13690535947'
MSG_INTERFACE_NAME_EXIST = '13421840550'
MSG_INTERFACE_EXIST = '13691781136'
MSG_INTERFACE_INVALID_VLAN_ID = '13421850371'
MSG_INTERFACE_NON_EXISTENT = '13691781134'
MSG_JOIN_DOMAIN = '13157007726'
MSG_UNJOIN_DOMAIN = '13157007723'
# Necessary to retry when VMAX database is locked for provisioning operation
MSG_CODE_RETRY = '13421840537'
IP_ALLOCATIONS = 2
CONTENT_TYPE_URLENCODE = {'Content-Type': 'application/x-www-form-urlencoded'}
XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
XML_NAMESPACE = 'http://www.emc.com/schemas/celerra/xml_api'
CIFS_ACL_FULLCONTROL = 'fullcontrol'
CIFS_ACL_READ = 'read'
SSH_DEFAULT_RETRY_PATTERN = r'Error 2201:.*: unable to acquire lock\(s\)'

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,83 @@
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
# 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 types
from oslo_config import cfg
from oslo_log import log
from oslo_utils import fnmatch
from oslo_utils import timeutils
CONF = cfg.CONF
LOG = log.getLogger(__name__)
def decorate_all_methods(decorator, debug_only=False):
if debug_only and not CONF.debug:
return lambda cls: cls
def _decorate_all_methods(cls):
for attr_name, attr_val in cls.__dict__.items():
if (isinstance(attr_val, types.FunctionType) and
not attr_name.startswith("_")):
setattr(cls, attr_name, decorator(attr_val))
return cls
return _decorate_all_methods
def log_enter_exit(func):
if not CONF.debug:
return func
def inner(self, *args, **kwargs):
LOG.debug("Entering %(cls)s.%(method)s.",
{'cls': self.__class__.__name__,
'method': func.__name__})
start = timeutils.utcnow()
ret = func(self, *args, **kwargs)
end = timeutils.utcnow()
LOG.debug("Exiting %(cls)s.%(method)s. "
"Spent %(duration)s sec. "
"Return %(return)s.",
{'cls': self.__class__.__name__,
'duration': timeutils.delta_seconds(start, end),
'method': func.__name__,
'return': ret})
return ret
return inner
def do_match_any(full, matcher_list):
"""Finds items that match any of the matchers.
:param full: Full item list
:param matcher_list: The list of matchers. Each matcher supports
Unix shell-style wildcards
:return: The matched items set and the unmatched items set
"""
matched = set()
not_matched = set()
full = set([item.strip() for item in full])
matcher_list = set([item.strip() for item in matcher_list])
for matcher in matcher_list:
for item in full:
if fnmatch.fnmatchcase(item, matcher):
matched.add(item)
not_matched = full - matched
return matched, not_matched

View File

@ -0,0 +1,317 @@
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
# 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 re
from lxml import etree
import six
class XMLAPIParser(object):
def __init__(self):
# The following Boolean acts as the flag for the common sub-element.
# For instance:
# <CifsServers>
# <li> server_1 </li>
# </CifsServers>
# <Alias>
# <li> interface_1 </li>
# </Alias>
self.is_QueryStatus = False
self.is_CifsServers = False
self.is_Aliases = False
self.is_MoverStatus = False
self.is_TaskResponse = False
self.is_Vdm = False
self.is_Interfaces = False
self.elt = {}
def _remove_ns(self, tag):
i = tag.find('}')
if i >= 0:
tag = tag[i + 1:]
return tag
def parse(self, xml):
result = {
'type': None,
'taskId': None,
'maxSeverity': None,
'objects': [],
'problems': [],
}
events = ("start", "end")
context = etree.iterparse(six.BytesIO(xml.encode('utf-8')),
events=events)
for action, elem in context:
self.tag = self._remove_ns(elem.tag)
func = self._get_func(action, self.tag)
if func in vars(XMLAPIParser):
if action == 'start':
eval('self.' + func)(elem, result)
elif action == 'end':
eval('self.' + func)()
return result
def _get_func(self, action, tag):
if tag == 'W2KServerData':
return action + '_' + 'w2k_server_data'
temp_list = re.sub(r"([A-Z])", r" \1", tag).split()
if temp_list:
func_name = action + '_' + '_'.join(temp_list)
else:
func_name = action + '_' + tag
return func_name.lower()
def _copy_property(self, source, target, property, list_property=None):
for key in property:
if key in source:
target[key] = source[key]
if list_property:
for key in list_property:
if key in source:
target[key] = source[key].split()
def _append_elm_property(self, elm, result, property, identifier):
for obj in result['objects']:
if (identifier in obj and identifier in elm.attrib and
elm.attrib[identifier] == obj[identifier]):
for key, value in elm.attrib.items():
if key in property:
obj[key] = value
def _append_element(self, elm, result, property, list_property,
identifier):
sub_elm = {}
self._copy_property(elm.attrib, sub_elm, property, list_property)
for obj in result['objects']:
if (identifier in obj and identifier in elm.attrib and
elm.attrib[identifier] == obj[identifier]):
if self.tag in obj:
obj[self.tag].append(sub_elm)
else:
obj[self.tag] = [sub_elm]
def start_task_response(self, elm, result):
self.is_TaskResponse = True
result['type'] = 'TaskResponse'
self._copy_property(elm.attrib, result, ['taskId'])
def end_task_response(self):
self.is_TaskResponse = False
def start_fault(self, elm, result):
result['type'] = 'Fault'
def start_status(self, elm, result):
if self.is_TaskResponse:
result['maxSeverity'] = elm.attrib['maxSeverity']
elif self.is_MoverStatus or self.is_Vdm:
self.elt['maxSeverity'] = elm.attrib['maxSeverity']
def start_query_status(self, elm, result):
self.is_QueryStatus = True
result['type'] = 'QueryStatus'
self._copy_property(elm.attrib, result, ['maxSeverity'])
def end_query_status(self):
self.is_QueryStatus = False
def start_problem(self, elm, result):
self.elt = {}
properties = ('message', 'messageCode')
self._copy_property(elm.attrib, self.elt, properties)
result['problems'].append(self.elt)
def start_description(self, elm, result):
self.elt['Description'] = elm.text
def start_action(self, elm, result):
self.elt['Action'] = elm.text
def start_diagnostics(self, elm, result):
self.elt['Diagnostics'] = elm.text
def start_file_system(self, elm, result):
self.elt = {}
property = (
'fileSystem',
'name',
'type',
'storages',
'volume',
'dataServicePolicies',
'internalUse',
)
list_property = ('storagePools',)
self._copy_property(elm.attrib, self.elt, property, list_property)
result['objects'].append(self.elt)
def start_file_system_capacity_info(self, elm, result):
property = ('volumeSize',)
identifier = 'fileSystem'
self._append_elm_property(elm, result, property, identifier)
def start_storage_pool(self, elm, result):
self.elt = {}
property = ('name', 'autoSize', 'usedSize', 'diskType', 'pool',
'dataServicePolicies', 'virtualProvisioning')
list_property = ('movers',)
self._copy_property(elm.attrib, self.elt, property, list_property)
result['objects'].append(self.elt)
def start_system_storage_pool_data(self, elm, result):
property = ('greedy', 'isBackendPool')
self._copy_property(elm.attrib, self.elt, property)
def start_mover(self, elm, result):
self.elt = {}
property = ('name', 'host', 'mover', 'role')
list_property = ('ntpServers', 'standbyFors', 'standbys')
self._copy_property(elm.attrib, self.elt, property, list_property)
result['objects'].append(self.elt)
def start_mover_status(self, elm, result):
self.is_MoverStatus = True
property = ('version', 'csTime', 'clock', 'timezone', 'uptime')
identifier = 'mover'
self._append_elm_property(elm, result, property, identifier)
def end_mover_status(self):
self.is_MoverStatus = False
def start_mover_dns_domain(self, elm, result):
property = ('name', 'protocol')
list_property = ('servers',)
identifier = 'mover'
self._append_element(elm, result, property, list_property, identifier)
def start_mover_interface(self, elm, result):
property = (
'name',
'device',
'up',
'ipVersion',
'netMask',
'ipAddress',
'vlanid',
)
identifier = 'mover'
self._append_element(elm, result, property, None, identifier)
def start_logical_network_device(self, elm, result):
property = ('name', 'type', 'speed')
list_property = ('interfaces',)
identifier = 'mover'
self._append_element(elm, result, property, list_property, identifier)
def start_vdm(self, elm, result):
self.is_Vdm = True
self.elt = {}
property = ('name', 'state', 'mover', 'vdm')
self._copy_property(elm.attrib, self.elt, property)
result['objects'].append(self.elt)
def end_vdm(self):
self.is_Vdm = False
def start_interfaces(self, elm, result):
self.is_Interfaces = True
self.elt['Interfaces'] = []
def end_interfaces(self):
self.is_Interfaces = False
def start_li(self, elm, result):
if self.is_CifsServers:
self.elt['CifsServers'].append(elm.text)
elif self.is_Aliases:
self.elt['Aliases'].append(elm.text)
elif self.is_Interfaces:
self.elt['Interfaces'].append(elm.text)
def start_cifs_server(self, elm, result):
self.elt = {}
property = ('type', 'localUsers', 'name', 'mover', 'moverIdIsVdm')
list_property = ('interfaces',)
self._copy_property(elm.attrib, self.elt, property, list_property)
result['objects'].append(self.elt)
def start_aliases(self, elm, result):
self.is_Aliases = True
self.elt['Aliases'] = []
def end_aliases(self):
self.is_Aliases = False
def start_w2k_server_data(self, elm, result):
property = ('domain', 'compName', 'domainJoined')
self._copy_property(elm.attrib, self.elt, property)
def start_cifs_share(self, elm, result):
self.elt = {}
property = ('path', 'fileSystem', 'name', 'mover', 'moverIdIsVdm')
self._copy_property(elm.attrib, self.elt, property)
result['objects'].append(self.elt)
def start_cifs_servers(self, elm, result):
self.is_CifsServers = True
self.elt['CifsServers'] = []
def end_cifs_servers(self):
self.is_CifsServers = False
def start_checkpoint(self, elm, result):
self.elt = {}
property = ('checkpointOf', 'name', 'checkpoint', 'state')
self._copy_property(elm.attrib, self.elt, property)
result['objects'].append(self.elt)
def start_mount(self, elm, result):
self.elt = {}
property = ('fileSystem', 'path', 'mover', 'moverIdIsVdm')
self._copy_property(elm.attrib, self.elt, property)
result['objects'].append(self.elt)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,224 @@
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
# 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.
from eventlet import greenthread
import mock
from oslo_concurrency import processutils
from six.moves.urllib import error as url_error # pylint: disable=E0611
from six.moves.urllib import request as url_request # pylint: disable=E0611
from manila import exception
from manila.share import configuration as conf
from manila.share.drivers.dell_emc.plugins.vmax import connector
from manila import test
from manila.tests.share.drivers.dell_emc.plugins.vmax import fakes
from manila.tests.share.drivers.dell_emc.plugins.vmax import utils as emc_utils
from manila import utils
class XMLAPIConnectorTestData(object):
FAKE_BODY = '<fakebody></fakebody>'
FAKE_RESP = '<Response></Response>'
FAKE_METHOD = 'fake_method'
FAKE_KEY = 'key'
FAKE_VALUE = 'value'
@staticmethod
def req_auth_url():
return 'https://' + fakes.FakeData.emc_nas_server + '/Login'
@staticmethod
def req_credential():
return (
'user=' + fakes.FakeData.emc_nas_login
+ '&password=' + fakes.FakeData.emc_nas_password
+ '&Login=Login'
)
@staticmethod
def req_url_encode():
return {'Content-Type': 'application/x-www-form-urlencoded'}
@staticmethod
def req_url():
return (
'https://'
+ fakes.FakeData.emc_nas_server
+ '/servlets/CelerraManagementServices'
)
XML_CONN_TD = XMLAPIConnectorTestData
class XMLAPIConnectorTest(test.TestCase):
@mock.patch.object(url_request, 'Request', mock.Mock())
def setUp(self):
super(XMLAPIConnectorTest, self).setUp()
emc_share_driver = fakes.FakeEMCShareDriver()
self.configuration = emc_share_driver.configuration
xml_socket = mock.Mock()
xml_socket.read = mock.Mock(return_value=XML_CONN_TD.FAKE_RESP)
opener = mock.Mock()
opener.open = mock.Mock(return_value=xml_socket)
with mock.patch.object(url_request, 'build_opener',
mock.Mock(return_value=opener)):
self.XmlConnector = connector.XMLAPIConnector(
configuration=self.configuration, debug=False)
expected_calls = [
mock.call(XML_CONN_TD.req_auth_url(),
XML_CONN_TD.req_credential(),
XML_CONN_TD.req_url_encode()),
]
url_request.Request.assert_has_calls(expected_calls)
def test_request_with_debug(self):
self.XmlConnector.debug = True
request = mock.Mock()
request.headers = {XML_CONN_TD.FAKE_KEY: XML_CONN_TD.FAKE_VALUE}
request.get_full_url = mock.Mock(
return_value=XML_CONN_TD.FAKE_VALUE)
with mock.patch.object(url_request, 'Request',
mock.Mock(return_value=request)):
rsp = self.XmlConnector.request(XML_CONN_TD.FAKE_BODY,
XML_CONN_TD.FAKE_METHOD)
self.assertEqual(XML_CONN_TD.FAKE_RESP, rsp)
def test_request_with_no_authorized_exception(self):
xml_socket = mock.Mock()
xml_socket.read = mock.Mock(return_value=XML_CONN_TD.FAKE_RESP)
hook = emc_utils.RequestSideEffect()
hook.append(ex=url_error.HTTPError(XML_CONN_TD.req_url(),
'403', 'fake_message', None, None))
hook.append(xml_socket)
hook.append(xml_socket)
self.XmlConnector.url_opener.open = mock.Mock(side_effect=hook)
self.XmlConnector.request(XML_CONN_TD.FAKE_BODY)
def test_request_with_general_exception(self):
hook = emc_utils.RequestSideEffect()
hook.append(ex=url_error.HTTPError(XML_CONN_TD.req_url(),
'error_code', 'fake_message',
None, None))
self.XmlConnector.url_opener.open = mock.Mock(side_effect=hook)
self.assertRaises(exception.ManilaException,
self.XmlConnector.request,
XML_CONN_TD.FAKE_BODY)
class MockSSH(object):
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
pass
class MockSSHPool(object):
def __init__(self):
self.ssh = MockSSH()
def item(self):
try:
return self.ssh
finally:
pass
class CmdConnectorTest(test.TestCase):
def setUp(self):
super(CmdConnectorTest, self).setUp()
self.configuration = conf.Configuration(None)
self.configuration.append_config_values = mock.Mock(return_value=0)
self.configuration.emc_nas_login = fakes.FakeData.emc_nas_login
self.configuration.emc_nas_password = fakes.FakeData.emc_nas_password
self.configuration.emc_nas_server = fakes.FakeData.emc_nas_server
self.sshpool = MockSSHPool()
with mock.patch.object(utils, "SSHPool",
mock.Mock(return_value=self.sshpool)):
self.CmdHelper = connector.SSHConnector(
configuration=self.configuration, debug=False)
utils.SSHPool.assert_called_once_with(
ip=fakes.FakeData.emc_nas_server,
port=22,
conn_timeout=None,
login=fakes.FakeData.emc_nas_login,
password=fakes.FakeData.emc_nas_password)
def test_run_ssh(self):
with mock.patch.object(processutils, "ssh_execute",
mock.Mock(return_value=('fake_output', ''))):
cmd_list = ['fake', 'cmd']
self.CmdHelper.run_ssh(cmd_list)
processutils.ssh_execute.assert_called_once_with(
self.sshpool.item(), 'fake cmd', check_exit_code=False)
def test_run_ssh_with_debug(self):
self.CmdHelper.debug = True
with mock.patch.object(processutils, "ssh_execute",
mock.Mock(return_value=('fake_output', ''))):
cmd_list = ['fake', 'cmd']
self.CmdHelper.run_ssh(cmd_list)
processutils.ssh_execute.assert_called_once_with(
self.sshpool.item(), 'fake cmd', check_exit_code=False)
@mock.patch.object(
processutils, "ssh_execute",
mock.Mock(side_effect=processutils.ProcessExecutionError))
def test_run_ssh_exception(self):
cmd_list = ['fake', 'cmd']
self.mock_object(greenthread, 'sleep', mock.Mock())
sshpool = MockSSHPool()
with mock.patch.object(utils, "SSHPool",
mock.Mock(return_value=sshpool)):
self.CmdHelper = connector.SSHConnector(self.configuration)
self.assertRaises(processutils.ProcessExecutionError,
self.CmdHelper.run_ssh,
cmd_list,
True)
utils.SSHPool.assert_called_once_with(
ip=fakes.FakeData.emc_nas_server,
port=22,
conn_timeout=None,
login=fakes.FakeData.emc_nas_login,
password=fakes.FakeData.emc_nas_password)
processutils.ssh_execute.assert_called_once_with(
sshpool.item(), 'fake cmd', check_exit_code=True)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
# 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 ddt
from manila.share.drivers.dell_emc.plugins.vmax import utils
from manila import test
@ddt.ddt
class VMAXUtilsTestCase(test.TestCase):
@ddt.data({'full': ['cge-1-0', 'cge-1-1', 'cge-3-0',
'cge-3-1', 'cge-12-3'],
'matchers': ['cge-?-0', 'cge-3*', 'foo'],
'matched': set(['cge-1-0', 'cge-3-0',
'cge-3-1']),
'unmatched': set(['cge-1-1', 'cge-12-3'])},
{'full': ['cge-1-0', 'cge-1-1'],
'matchers': ['cge-1-0'],
'matched': set(['cge-1-0']),
'unmatched': set(['cge-1-1'])},
{'full': ['cge-1-0', 'cge-1-1'],
'matchers': ['foo'],
'matched': set([]),
'unmatched': set(['cge-1-0', 'cge-1-1'])})
@ddt.unpack
def test_do_match_any(self, full, matchers, matched, unmatched):
real_matched, real_unmatched = utils.do_match_any(
full, matchers)
self.assertEqual(matched, real_matched)
self.assertEqual(unmatched, real_unmatched)

View File

@ -0,0 +1,167 @@
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
# 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 doctest
from lxml import doctestcompare
import mock
import six
CHECKER = doctestcompare.LXMLOutputChecker()
PARSE_XML = doctest.register_optionflag('PARSE_XML')
class RequestSideEffect(object):
def __init__(self):
self.actions = []
self.started = False
def append(self, resp=None, ex=None):
if not self.started:
self.actions.append((resp, ex))
def __call__(self, *args, **kwargs):
if not self.started:
self.started = True
self.actions.reverse()
item = self.actions.pop()
if item[1]:
raise item[1]
else:
return item[0]
class SSHSideEffect(object):
def __init__(self):
self.actions = []
self.started = False
def append(self, resp=None, err=None, ex=None):
if not self.started:
self.actions.append((resp, err, ex))
def __call__(self, rel_url, req_data=None, method=None,
return_rest_err=True, *args, **kwargs):
if not self.started:
self.started = True
self.actions.reverse()
item = self.actions.pop()
if item[2]:
raise item[2]
else:
if return_rest_err:
return item[0:2]
else:
return item[1]
class EMCMock(mock.Mock):
def _get_req_from_call(self, call):
if len(call) == 3:
return call[1][0]
elif len(call) == 2:
return call[0][0]
def assert_has_calls(self, calls):
if len(calls) != len(self.mock_calls):
raise AssertionError(
'Mismatch error.\nExpected: %r\n'
'Actual: %r' % (calls, self.mock_calls)
)
iter_expect = iter(calls)
iter_actual = iter(self.mock_calls)
while True:
try:
expect = self._get_req_from_call(next(iter_expect))
actual = self._get_req_from_call(next(iter_actual))
except StopIteration:
return True
if not isinstance(expect, six.binary_type):
expect = six.b(expect)
if not isinstance(actual, six.binary_type):
actual = six.b(actual)
if not CHECKER.check_output(expect, actual, PARSE_XML):
raise AssertionError(
'Mismatch error.\nExpected: %r\n'
'Actual: %r' % (calls, self.mock_calls)
)
class EMCNFSShareMock(mock.Mock):
def assert_has_calls(self, calls):
if len(calls) != len(self.mock_calls):
raise AssertionError(
'Mismatch error.\nExpected: %r\n'
'Actual: %r' % (calls, self.mock_calls)
)
iter_expect = iter(calls)
iter_actual = iter(self.mock_calls)
while True:
try:
expect = next(iter_expect)[1][0]
actual = next(iter_actual)[1][0]
except StopIteration:
return True
if not self._option_check(expect, actual):
raise AssertionError(
'Mismatch error.\nExpected: %r\n'
'Actual: %r' % (calls, self.mock_calls)
)
def _option_parser(self, option):
option_map = {}
for item in option.split(','):
key, value = item.split('=')
option_map[key] = value
return option_map
@staticmethod
def _opt_value_from_map(opt_map, key):
value = opt_map.get(key)
if value:
ret = set(value.split(':'))
else:
ret = set()
return ret
def _option_check(self, expect, actual):
if '-option' in actual and '-option' in expect:
exp_option = expect[expect.index('-option') + 1]
act_option = actual[actual.index('-option') + 1]
exp_opt_map = self._option_parser(exp_option)
act_opt_map = self._option_parser(act_option)
for key in exp_opt_map:
exp_set = self._opt_value_from_map(exp_opt_map, key)
act_set = self._opt_value_from_map(act_opt_map, key)
if exp_set != act_set:
return False
return True
def patch_get_managed_ports(*arg, **kwargs):
return mock.patch('manila.share.drivers.dell_emc.plugins.vmax.connection.'
'VMAXStorageConnection.get_managed_ports',
mock.Mock(*arg, **kwargs))

View File

@ -0,0 +1,3 @@
---
features:
- Support for VMAX in Manila.

View File

@ -62,6 +62,7 @@ manila.share.drivers.dell_emc.plugins =
vnx = manila.share.drivers.dell_emc.plugins.vnx.connection:VNXStorageConnection
unity = manila.share.drivers.dell_emc.plugins.unity.connection:UnityStorageConnection
isilon = manila.share.drivers.dell_emc.plugins.isilon.isilon:IsilonStorageConnection
vmax = manila.share.drivers.dell_emc.plugins.vmax.connection:VMAXStorageConnection
manila.tests.scheduler.fakes =
FakeWeigher1 = manila.tests.scheduler.fakes:FakeWeigher1
FakeWeigher2 = manila.tests.scheduler.fakes:FakeWeigher2