Adds Oracle ZFSSA driver for Manila

The driver allows Oracle ZFSSA to be a storage resource for Manila.
It uses the REST API to communicate with ZFSSA and perform the following:
* Create/delete NFS/CIFS shares.
* Create shares from snapshots.
* Create/delete snapshots.
* Allow/deny IP access to an NFS share.
* Get share status.

DocImpact
Change-Id: I2e4201842ee2fa2825bad3e14ff1615670a18d31
Implements: blueprint oracle-zfssa-driver
This commit is contained in:
Diem Tran 2014-12-05 15:18:29 -05:00
parent 28f311c9ad
commit 6c4f6f6340
9 changed files with 1814 additions and 0 deletions

View File

@ -104,6 +104,7 @@ _global_opt_lists = [
manila.share.drivers.ibm.gpfs.gpfs_share_opts,
manila.share.drivers.netapp.cluster_mode.NETAPP_NAS_OPTS,
manila.share.drivers.service_instance.server_opts,
manila.share.drivers.zfssa.zfssashare.ZFSSA_OPTS,
manila.share.manager.share_manager_opts,
manila.volume._volume_opts,
manila.volume.cinder.cinder_opts,

View File

View File

@ -0,0 +1,369 @@
# Copyright (c) 2014, Oracle and/or its affiliates. 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.
"""
ZFS Storage Appliance REST API Client Programmatic Interface
TODO(diemtran): this module needs to be placed in a library common to OpenStack
services. When this happens, the file should be removed from Manila code
base and imported from the relevant library.
"""
import httplib
import time
from oslo.serialization import jsonutils
import six
# pylint: disable=E0611,F0401
from six.moves.urllib import error as urlerror
from six.moves.urllib import request as urlrequest
def log_debug_msg(obj, message):
if obj.log_function:
obj.log_function(message)
class Status(object):
"""Result HTTP Status."""
#: Request return OK
OK = httplib.OK # pylint: disable=invalid-name
#: New resource created successfully
CREATED = httplib.CREATED
#: Command accepted
ACCEPTED = httplib.ACCEPTED
#: Command returned OK but no data will be returned
NO_CONTENT = httplib.NO_CONTENT
#: Bad Request
BAD_REQUEST = httplib.BAD_REQUEST
#: User is not authorized
UNAUTHORIZED = httplib.UNAUTHORIZED
#: The request is not allowed
FORBIDDEN = httplib.FORBIDDEN
#: The requested resource was not found
NOT_FOUND = httplib.NOT_FOUND
#: The request is not allowed
NOT_ALLOWED = httplib.METHOD_NOT_ALLOWED
#: Request timed out
TIMEOUT = httplib.REQUEST_TIMEOUT
#: Invalid request
CONFLICT = httplib.CONFLICT
#: Service Unavailable
BUSY = httplib.SERVICE_UNAVAILABLE
class RestResult(object):
"""Result from a REST API operation."""
def __init__(self, logfunc=None, response=None, err=None):
"""Initialize a RestResult containing the results from a REST call.
:param logfunc: debug log function.
:param response: HTTP response.
:param err: HTTP error.
"""
self.response = response
self.log_function = logfunc
self.error = err
self.data = ""
self.status = 0
if self.response:
self.status = self.response.getcode()
result = self.response.read()
while result:
self.data += result
result = self.response.read()
if self.error:
self.status = self.error.code
self.data = httplib.responses[self.status]
log_debug_msg(self, 'Response code: %s' % self.status)
log_debug_msg(self, 'Response data: %s' % self.data)
def get_header(self, name):
"""Get an HTTP header with the given name from the results.
:param name: HTTP header name.
:return: The header value or None if no value is found.
"""
if self.response is None:
return None
info = self.response.info()
return info.getheader(name)
class RestClientError(Exception):
"""Exception for ZFS REST API client errors."""
def __init__(self, status, name="ERR_INTERNAL", message=None):
"""Create a REST Response exception.
:param status: HTTP response status.
:param name: The name of the REST API error type.
:param message: Descriptive error message returned from REST call.
"""
super(RestClientError, self).__init__(message)
self.code = status
self.name = name
self.msg = message
if status in httplib.responses:
self.msg = httplib.responses[status]
def __str__(self):
return "%d %s %s" % (self.code, self.name, self.msg)
class RestClientURL(object): # pylint: disable=R0902
"""ZFSSA urllib client."""
def __init__(self, url, logfunc=None, **kwargs):
"""Initialize a REST client.
:param url: The ZFSSA REST API URL.
:key session: HTTP Cookie value of x-auth-session obtained from a
normal BUI login.
:key timeout: Time in seconds to wait for command to complete.
(Default is 60 seconds).
"""
self.url = url
self.log_function = logfunc
self.local = kwargs.get("local", False)
self.base_path = kwargs.get("base_path", "/api")
self.timeout = kwargs.get("timeout", 60)
self.headers = None
if kwargs.get('session'):
self.headers['x-auth-session'] = kwargs.get('session')
self.headers = {"content-type": "application/json"}
self.do_logout = False
self.auth_str = None
def _path(self, path, base_path=None):
"""Build rest url path."""
if path.startswith("http://") or path.startswith("https://"):
return path
if base_path is None:
base_path = self.base_path
if not path.startswith(base_path) and not (
self.local and ("/api" + path).startswith(base_path)):
path = "%s%s" % (base_path, path)
if self.local and path.startswith("/api"):
path = path[4:]
return self.url + path
def _authorize(self):
"""Performs authorization setting x-auth-session."""
self.headers['authorization'] = 'Basic %s' % self.auth_str
if 'x-auth-session' in self.headers:
del self.headers['x-auth-session']
try:
result = self.post("/access/v1")
del self.headers['authorization']
if result.status == httplib.CREATED:
self.headers['x-auth-session'] = \
result.get_header('x-auth-session')
self.do_logout = True
log_debug_msg(self, ('ZFSSA version: %s')
% result.get_header('x-zfssa-version'))
elif result.status == httplib.NOT_FOUND:
raise RestClientError(result.status, name="ERR_RESTError",
message=("REST Not Available:"
"Please Upgrade"))
except RestClientError as err:
del self.headers['authorization']
raise err
def login(self, auth_str):
"""Login to an appliance using a user name and password.
Start a session like what is done logging into the BUI. This is not a
requirement to run REST commands, since the protocol is stateless.
What is does is set up a cookie session so that some server side
caching can be done. If login is used remember to call logout when
finished.
:param auth_str: Authorization string (base64).
"""
self.auth_str = auth_str
self._authorize()
def logout(self):
"""Logout of an appliance."""
result = None
try:
result = self.delete("/access/v1", base_path="/api")
except RestClientError:
pass
self.headers.clear()
self.do_logout = False
return result
def islogin(self):
"""return if client is login."""
return self.do_logout
@staticmethod
def mkpath(*args, **kwargs):
"""Make a path?query string for making a REST request.
:cmd_params args: The path part.
:cmd_params kwargs: The query part.
"""
buf = six.StringIO()
query = "?"
for arg in args:
buf.write("/")
buf.write(arg)
for k in kwargs:
buf.write(query)
if query == "?":
query = "&"
buf.write(k)
buf.write("=")
buf.write(kwargs[k])
return buf.getvalue()
# pylint: disable=R0912
def request(self, path, request, body=None, **kwargs):
"""Make an HTTP request and return the results.
:param path: Path used with the initialized URL to make a request.
:param request: HTTP request type (GET, POST, PUT, DELETE).
:param body: HTTP body of request.
:key accept: Set HTTP 'Accept' header with this value.
:key base_path: Override the base_path for this request.
:key content: Set HTTP 'Content-Type' header with this value.
"""
out_hdrs = dict.copy(self.headers)
if kwargs.get("accept"):
out_hdrs['accept'] = kwargs.get("accept")
if body:
if isinstance(body, dict):
body = six.text_type(jsonutils.dumps(body))
if body and len(body):
out_hdrs['content-length'] = len(body)
zfssaurl = self._path(path, kwargs.get("base_path"))
req = urlrequest.Request(zfssaurl, body, out_hdrs)
req.get_method = lambda: request
maxreqretries = kwargs.get("maxreqretries", 10)
retry = 0
response = None
log_debug_msg(self, 'Request: %s %s' % (request, zfssaurl))
log_debug_msg(self, 'Out headers: %s' % out_hdrs)
if body and body != '':
log_debug_msg(self, 'Body: %s' % body)
while retry < maxreqretries:
try:
response = urlrequest.urlopen(req, timeout=self.timeout)
except urlerror.HTTPError as err:
if err.code == httplib.NOT_FOUND:
log_debug_msg(self, 'REST Not Found: %s' % err.code)
else:
log_debug_msg(self, ('REST Not Available: %s') % err.code)
if (err.code == httplib.SERVICE_UNAVAILABLE and
retry < maxreqretries):
retry += 1
time.sleep(1)
log_debug_msg(self, ('Server Busy retry request: %s')
% retry)
continue
if ((err.code == httplib.UNAUTHORIZED or
err.code == httplib.INTERNAL_SERVER_ERROR) and
'/access/v1' not in zfssaurl):
try:
log_debug_msg(self, ('Authorizing request: '
'%(zfssaurl)s'
'retry: %(retry)d .')
% {'zfssaurl': zfssaurl,
'retry': retry})
self._authorize()
req.add_header('x-auth-session',
self.headers['x-auth-session'])
except RestClientError:
log_debug_msg(self, ('Cannot authorize.'))
retry += 1
time.sleep(1)
continue
return RestResult(self.log_function, err=err)
except urlerror.URLError as err:
log_debug_msg(self, ('URLError: %s') % err.reason)
raise RestClientError(-1, name="ERR_URLError",
message=err.reason)
break
if ((response and
response.getcode() == httplib.SERVICE_UNAVAILABLE) and
retry >= maxreqretries):
raise RestClientError(response.getcode(), name="ERR_HTTPError",
message="REST Not Available: Disabled")
return RestResult(self.log_function, response=response)
def get(self, path, **kwargs):
"""Make an HTTP GET request.
:param path: Path to resource.
"""
return self.request(path, "GET", **kwargs)
def post(self, path, body="", **kwargs):
"""Make an HTTP POST request.
:param path: Path to resource.
:param body: Post data content.
"""
return self.request(path, "POST", body, **kwargs)
def put(self, path, body="", **kwargs):
"""Make an HTTP PUT request.
:param path: Path to resource.
:param body: Put data content.
"""
return self.request(path, "PUT", body, **kwargs)
def delete(self, path, **kwargs):
"""Make an HTTP DELETE request.
:param path: Path to resource that will be deleted.
"""
return self.request(path, "DELETE", **kwargs)
def head(self, path, **kwargs):
"""Make an HTTP HEAD request.
:param path: Path to resource.
"""
return self.request(path, "HEAD", **kwargs)

View File

@ -0,0 +1,387 @@
# Copyright (c) 2014, Oracle and/or its affiliates. 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.
"""
ZFS Storage Appliance Proxy
"""
from oslo.serialization import jsonutils
from manila import exception
from manila.i18n import _
from manila.i18n import _LE
from manila.openstack.common import log
from manila.share.drivers.zfssa import restclient
LOG = log.getLogger(__name__)
def factory_restclient(url, logfunc, **kwargs):
return restclient.RestClientURL(url, logfunc, **kwargs)
class ZFSSAApi(object):
"""ZFSSA API proxy class."""
pools_path = '/api/storage/v1/pools'
pool_path = pools_path + '/%s'
projects_path = pool_path + '/projects'
project_path = projects_path + '/%s'
shares_path = project_path + '/filesystems'
share_path = shares_path + '/%s'
snapshots_path = share_path + '/snapshots'
snapshot_path = snapshots_path + '/%s'
clone_path = snapshot_path + '/clone'
service_path = '/api/service/v1/services/%s/enable'
def __init__(self):
self.host = None
self.url = None
self.rclient = None
def __del__(self):
if self.rclient:
del self.rclient
def rest_get(self, path, expected):
ret = self.rclient.get(path)
if ret.status != expected:
exception_msg = (_('Rest call to %(host)s %(path)s failed.'
'Status: %(status)d Message: %(data)s')
% {'host': self.host,
'path': path,
'status': ret.status,
'data': ret.data})
LOG.error(exception_msg)
raise exception.ShareBackendException(msg=exception_msg)
return ret
def _is_pool_owned(self, pdata):
"""returns True if the pool's owner is the same as the host."""
svc = '/api/system/v1/version'
ret = self.rest_get(svc, restclient.Status.OK)
vdata = jsonutils.loads(ret.data)
return (vdata['version']['asn'] == pdata['pool']['asn'] and
vdata['version']['nodename'] == pdata['pool']['owner'])
def set_host(self, host, timeout=None):
self.host = host
self.url = "https://%s:215" % self.host
self.rclient = factory_restclient(self.url, LOG.debug, timeout=timeout)
def login(self, auth_str):
"""Login to the appliance."""
if self.rclient and not self.rclient.islogin():
self.rclient.login(auth_str)
def enable_service(self, service):
"""Enable the specified service."""
svc = self.service_path % service
ret = self.rclient.put(svc)
if ret.status != restclient.Status.ACCEPTED:
exception_msg = (_("Cannot enable %s service.") % service)
raise exception.ShareBackendException(msg=exception_msg)
def verify_avail_space(self, pool, project, share, size):
"""Check if there is enough space available to a new share."""
self.verify_project(pool, project)
avail = self.get_project_stats(pool, project)
if avail < size:
exception_msg = (_('Error Creating '
'Share: %(share)s on '
'Pool: %(pool)s. '
'Not enough space.')
% {'share': share,
'pool': pool})
raise exception.ShareBackendException(msg=exception_msg)
def get_pool_stats(self, pool):
"""Get space_available and used properties of a pool.
returns (avail, used).
"""
svc = self.pool_path % pool
ret = self.rclient.get(svc)
if ret.status != restclient.Status.OK:
exception_msg = (_('Error Getting Pool Stats: '
'Pool: %(pool)s '
'Return code: %(ret.status)d '
'Message: %(ret.data)s.')
% {'pool': pool,
'ret.status': ret.status,
'ret.data': ret.data})
raise exception.InvalidInput(reason=exception_msg)
val = jsonutils.loads(ret.data)
if not self._is_pool_owned(val):
exception_msg = (_('Error Pool ownership: '
'Pool %(pool)s is not owned '
'by %(host)s.')
% {'pool': pool,
'host': self.host})
raise exception.InvalidInput(reason=pool)
avail = val['pool']['usage']['available']
used = val['pool']['usage']['used']
return avail, used
def get_project_stats(self, pool, project):
"""Get space_available of a project.
Used to check whether a project has enough space (after reservation)
or not.
"""
svc = self.project_path % (pool, project)
ret = self.rclient.get(svc)
if ret.status != restclient.Status.OK:
exception_msg = (_('Error Getting Project Stats: '
'Pool: %(pool)s '
'Project: %(project)s '
'Return code: %(ret.status)d '
'Message: %(ret.data)s.')
% {'pool': pool,
'project': project,
'ret.status': ret.status,
'ret.data': ret.data})
raise exception.InvalidInput(reason=exception_msg)
val = jsonutils.loads(ret.data)
avail = val['project']['space_available']
return avail
def create_project(self, pool, project, arg):
"""Create a project on a pool. Check first whether the pool exists."""
self.verify_pool(pool)
svc = self.project_path % (pool, project)
ret = self.rclient.get(svc)
if ret.status != restclient.Status.OK:
svc = self.projects_path % pool
ret = self.rclient.post(svc, arg)
if ret.status != restclient.Status.CREATED:
exception_msg = (_('Error Creating Project: '
'%(project)s on '
'Pool: %(pool)s '
'Return code: %(ret.status)d '
'Message: %(ret.data)s .')
% {'project': project,
'pool': pool,
'ret.status': ret.status,
'ret.data': ret.data})
raise exception.ShareBackendException(msg=exception_msg)
def verify_pool(self, pool):
"""Checks whether pool exists."""
svc = self.pool_path % pool
self.rest_get(svc, restclient.Status.OK)
def verify_project(self, pool, project):
"""Checks whether project exists."""
svc = self.project_path % (pool, project)
ret = self.rest_get(svc, restclient.Status.OK)
return ret
def create_share(self, pool, project, share):
"""Create a share in the specified pool and project."""
self.verify_avail_space(pool, project, share, share['quota'])
svc = self.share_path % (pool, project, share['name'])
ret = self.rclient.get(svc)
if ret.status != restclient.Status.OK:
svc = self.shares_path % (pool, project)
ret = self.rclient.post(svc, share)
if ret.status != restclient.Status.CREATED:
exception_msg = (_('Error Creating '
'Share: %(name)s '
'Return code: %(ret.status)d '
'Message: %(ret.data)s.')
% {'name': share['name'],
'ret.status': ret.status,
'ret.data': ret.data})
raise exception.ShareBackendException(msg=exception_msg)
else:
exception_msg = (_('Share with name %s already exists.')
% share['name'])
raise exception.ShareBackendException(msg=exception_msg)
def get_share(self, pool, project, share):
"""Return share properties."""
svc = self.share_path % (pool, project, share)
ret = self.rest_get(svc, restclient.Status.OK)
val = jsonutils.loads(ret.data)
return val['filesystem']
def modify_share(self, pool, project, share, arg):
"""Modify a set of properties of a share."""
svc = self.share_path % (pool, project, share)
ret = self.rclient.put(svc, arg)
if ret.status != restclient.Status.ACCEPTED:
exception_msg = (_('Error modifying %(arg)s '
' of share %(id)s.')
% {'arg': arg,
'id': share})
raise exception.ShareBackendException(msg=exception_msg)
def delete_share(self, pool, project, share):
"""Delete a share.
The function assumes the share has no clone or snapshot.
"""
svc = self.share_path % (pool, project, share)
ret = self.rclient.delete(svc)
if ret.status != restclient.Status.NO_CONTENT:
exception_msg = (_LE('Error Deleting '
'Share: %(share)s to '
'Pool: %(pool)s '
'Project: %(project)s '
'Return code: %(ret.status)d '
'Message: %(ret.data)s.'),
{'share': share,
'pool': pool,
'project': project,
'ret.status': ret.status,
'ret.data': ret.data})
LOG.error(exception_msg)
def create_snapshot(self, pool, project, share, snapshot):
"""Create a snapshot of the given share."""
svc = self.snapshots_path % (pool, project, share)
arg = {'name': snapshot}
ret = self.rclient.post(svc, arg)
if ret.status != restclient.Status.CREATED:
exception_msg = (_('Error Creating '
'Snapshot: %(snapshot)s on'
'Share: %(share)s to '
'Pool: %(pool)s '
'Project: %(project)s '
'Return code: %(ret.status)d '
'Message: %(ret.data)s.')
% {'snapshot': snapshot,
'share': share,
'pool': pool,
'project': project,
'ret.status': ret.status,
'ret.data': ret.data})
raise exception.ShareBackendException(msg=exception_msg)
def delete_snapshot(self, pool, project, share, snapshot):
"""Delete a snapshot that has no clone."""
svc = self.snapshot_path % (pool, project, share, snapshot)
ret = self.rclient.delete(svc)
if ret.status != restclient.Status.NO_CONTENT:
exception_msg = (_('Error Deleting '
'Snapshot: %(snapshot)s on '
'Share: %(share)s to '
'Pool: %(pool)s '
'Project: %(project)s '
'Return code: %(ret.status)d '
'Message: %(ret.data)s.')
% {'snapshot': snapshot,
'share': share,
'pool': pool,
'project': project,
'ret.status': ret.status,
'ret.data': ret.data})
LOG.error(exception_msg)
raise exception.ShareBackendException(msg=exception_msg)
def clone_snapshot(self, pool, project, snapshot, clone, arg):
"""Create a new share from the given snapshot."""
self.verify_avail_space(pool, project, clone['id'], clone['size'])
svc = self.clone_path % (pool, project,
snapshot['share_id'],
snapshot['id'])
ret = self.rclient.put(svc, arg)
if ret.status != restclient.Status.CREATED:
exception_msg = (_('Error Cloning '
'Snapshot: %(snapshot)s on '
'Share: %(share)s of '
'Pool: %(pool)s '
'Project: %(project)s '
'Return code: %(ret.status)d '
'Message: %(ret.data)s.')
% {'snapshot': snapshot['id'],
'share': snapshot['share_id'],
'pool': pool,
'project': project,
'ret.status': ret.status,
'ret.data': ret.data})
LOG.error(exception_msg)
raise exception.ShareBackendException(msg=exception_msg)
def has_clones(self, pool, project, share, snapshot):
"""Check whether snapshot has existing clones."""
svc = self.snapshot_path % (pool, project, share, snapshot)
ret = self.rest_get(svc, restclient.Status.OK)
val = jsonutils.loads(ret.data)
return val['snapshot']['numclones'] != 0
def allow_access_nfs(self, pool, project, share, access):
"""Allow an IP access to a share through NFS."""
if access['access_type'] != 'ip':
reason = _('Only ip access type allowed.')
raise exception.InvalidShareAccess(reason)
ip = access['access_to']
details = self.get_share(pool, project, share)
sharenfs = details['sharenfs']
if sharenfs == 'on' or sharenfs == 'rw':
LOG.debug('Share %s has read/write permission'
'open to all.', share)
return
if sharenfs == 'off':
sharenfs = 'sec=sys'
if ip in sharenfs:
LOG.debug('Access to Share %(share)s via NFS '
'already granted to %(ip)s.',
{'share': share,
'ip': ip})
return
entry = (',rw=@%s' % ip)
if '/' not in ip:
entry = "%s/32" % entry
arg = {'sharenfs': sharenfs + entry}
self.modify_share(pool, project, share, arg)
def deny_access_nfs(self, pool, project, share, access):
"""Denies access of an IP to a share through NFS.
Since sharenfs property allows a combination of mutiple syntaxes:
sharenfs="sec=sys,rw=@first_ip,rw=@second_ip"
sharenfs="sec=sys,rw=@first_ip:@second_ip"
sharenfs="sec=sys,rw=@first_ip:@second_ip,rw=@third_ip"
The function checks what syntax is used and remove the IP accordingly.
"""
if access['access_type'] != 'ip':
reason = _('Only ip access type allowed.')
raise exception.InvalidShareAccess(reason)
ip = access['access_to']
entry = ('@%s' % ip)
if '/' not in ip:
entry = "%s/32" % entry
details = self.get_share(pool, project, share)
if entry not in details['sharenfs']:
LOG.debug('IP %(ip)s does not have access '
'to Share %(share)s via NFS.',
{'ip': ip,
'share': share})
return
sharenfs = str(details['sharenfs'])
argval = ''
if sharenfs.find((',rw=%s:' % entry)) >= 0:
argval = sharenfs.replace(('%s:' % entry), '')
elif sharenfs.find((',rw=%s' % entry)) >= 0:
argval = sharenfs.replace((',rw=%s' % entry), '')
elif sharenfs.find((':%s' % entry)) >= 0:
argval = sharenfs.replace((':%s' % entry), '')
arg = {'sharenfs': argval}
LOG.debug('deny_access: %s', argval)
self.modify_share(pool, project, share, arg)

View File

@ -0,0 +1,331 @@
# Copyright (c) 2014, Oracle and/or its affiliates. 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.
"""
ZFS Storage Appliance Manila Share Driver
"""
import base64
from oslo.config import cfg
from oslo.utils import units
from manila import exception
from manila.i18n import _
from manila.i18n import _LE
from manila.openstack.common import log
from manila.share import driver
from manila.share.drivers.zfssa import zfssarest
ZFSSA_OPTS = [
cfg.StrOpt('zfssa_host',
help='ZFSSA management IP address.'),
cfg.StrOpt('zfssa_data_ip',
help='IP address for data.'),
cfg.StrOpt('zfssa_auth_user',
help='ZFSSA management authorized username.'),
cfg.StrOpt('zfssa_auth_password',
help='ZFSSA management authorized userpassword.'),
cfg.StrOpt('zfssa_pool',
help='ZFSSA storage pool name.'),
cfg.StrOpt('zfssa_project',
help='ZFSSA project name.'),
cfg.StrOpt('zfssa_nas_checksum', default='fletcher4',
help='Controls checksum used for data blocks.'),
cfg.StrOpt('zfssa_nas_compression', default='off',
help='Data compression-off, lzjb, gzip-2, gzip, gzip-9.'),
cfg.StrOpt('zfssa_nas_logbias', default='latency',
help='Controls behavior when servicing synchronous writes.'),
cfg.StrOpt('zfssa_nas_mountpoint', default='',
help='Location of project in ZFS/SA.'),
cfg.StrOpt('zfssa_nas_quota_snap', default='true',
help='Controls whether a share quota includes snapshot.'),
cfg.StrOpt('zfssa_nas_rstchown', default='true',
help='Controls whether file ownership can be changed.'),
cfg.StrOpt('zfssa_nas_vscan', default='false',
help='Controls whether the share is scanned for viruses.'),
cfg.StrOpt('zfssa_rest_timeout',
help='REST connection timeout (in seconds).')
]
cfg.CONF.register_opts(ZFSSA_OPTS)
LOG = log.getLogger(__name__)
def factory_zfssa():
return zfssarest.ZFSSAApi()
class ZFSSAShareDriver(driver.ShareDriver):
"""ZFSSA share driver: Supports NFS and CIFS protocols.
Uses ZFSSA RESTful API to create shares and snapshots on backend.
API version history:
1.0 - Initial version.
"""
VERSION = '1.0.0'
PROTOCOL = 'NFS_CIFS'
def __init__(self, *args, **kwargs):
super(ZFSSAShareDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(ZFSSA_OPTS)
self.zfssa = None
self._stats = None
self.mountpoint = '/export/'
lcfg = self.configuration
required = [
'zfssa_host',
'zfssa_data_ip',
'zfssa_auth_user',
'zfssa_auth_password',
'zfssa_pool',
'zfssa_project'
]
for prop in required:
if not getattr(lcfg, prop, None):
exception_msg = _('%s is required in manila.conf') % prop
LOG.error(exception_msg)
raise exception.InvalidParameterValue(exception_msg)
self.default_args = {
'compression': lcfg.zfssa_nas_compression,
'logbias': lcfg.zfssa_nas_logbias,
'checksum': lcfg.zfssa_nas_checksum,
'vscan': lcfg.zfssa_nas_vscan,
'rstchown': lcfg.zfssa_nas_rstchown,
}
self.share_args = {
'sharedav': 'off',
'shareftp': 'off',
'sharesftp': 'off',
'sharetftp': 'off',
'root_permissions': '777',
'sharenfs': 'sec=sys',
'sharesmb': 'off',
'quota_snap': self.configuration.zfssa_nas_quota_snap,
'reservation_snap': self.configuration.zfssa_nas_quota_snap,
}
def do_setup(self, context):
"""Login, create project, no sharing option enabled."""
lcfg = self.configuration
LOG.debug("Connecting to host: %s.", lcfg.zfssa_host)
self.zfssa = factory_zfssa()
self.zfssa.set_host(lcfg.zfssa_host, timeout=lcfg.zfssa_rest_timeout)
auth_str = base64.encodestring('%s:%s' % (lcfg.zfssa_auth_user,
lcfg.zfssa_auth_password))[:-1]
self.zfssa.login(auth_str)
if lcfg.zfssa_nas_mountpoint == '':
self.mountpoint += lcfg.zfssa_project
else:
self.mountpoint += lcfg.zfssa_nas_mountpoint
arg = {
'name': lcfg.zfssa_project,
'sharesmb': 'off',
'sharenfs': 'off',
'mountpoint': self.mountpoint,
}
arg.update(self.default_args)
self.zfssa.create_project(lcfg.zfssa_pool, lcfg.zfssa_project, arg)
self.zfssa.enable_service('nfs')
self.zfssa.enable_service('smb')
def check_for_setup_error(self):
"""Check for properly configured pool, project."""
lcfg = self.configuration
LOG.debug("Verifying pool %s.", lcfg.zfssa_pool)
self.zfssa.verify_pool(lcfg.zfssa_pool)
LOG.debug("Verifying project %s.", lcfg.zfssa_project)
self.zfssa.verify_project(lcfg.zfssa_pool, lcfg.zfssa_project)
def _export_location(self, share):
"""Export share's location based on protocol used."""
lcfg = self.configuration
arg = {
'host': lcfg.zfssa_data_ip,
'mountpoint': self.mountpoint,
'name': share['id'],
}
location = ''
proto = share['share_proto']
if proto == 'NFS':
location = ("%(host)s:%(mountpoint)s/%(name)s" % arg)
elif proto == 'CIFS':
location = ("\\\\%(host)s\\%(name)s" % arg)
else:
exception_msg = _('Protocol %s is not supported.') % proto
LOG.error(exception_msg)
raise exception.InvalidParameterValue(exception_msg)
LOG.debug("Export location: %s.", location)
return location
def create_arg(self, size):
size = units.Gi * int(size)
arg = {
'quota': size,
'reservation': size,
}
arg.update(self.share_args)
return arg
def create_share(self, context, share, share_server=None):
"""Create a share and export it based on protocol used.
The created share inherits properties from its project.
"""
lcfg = self.configuration
arg = self.create_arg(share['size'])
arg.update(self.default_args)
arg.update({'name': share['id']})
if share['share_proto'].startswith('CIFS'):
arg.update({'sharesmb': 'on'})
LOG.debug("ZFSSAShareDriver.create_share: id=%(name)s, size=%(quota)s",
{'name': arg['name'],
'quota': arg['quota']})
self.zfssa.create_share(lcfg.zfssa_pool, lcfg.zfssa_project, arg)
return self._export_location(share)
def delete_share(self, context, share, share_server=None):
"""Delete a share.
Shares with existing snapshots can't be deleted.
"""
LOG.debug("ZFSSAShareDriver.delete_share: id=%s", share['id'])
lcfg = self.configuration
self.zfssa.delete_share(lcfg.zfssa_pool,
lcfg.zfssa_project,
share['id'])
def create_snapshot(self, context, snapshot, share_server=None):
"""Creates a snapshot of the snapshot['share_id']."""
LOG.debug("ZFSSAShareDriver.create_snapshot: "
"id=%(snap)s share=%(share)s",
{'snap': snapshot['id'],
'share': snapshot['share_id']})
lcfg = self.configuration
self.zfssa.create_snapshot(lcfg.zfssa_pool,
lcfg.zfssa_project,
snapshot['share_id'],
snapshot['id'])
def create_share_from_snapshot(self, context, share, snapshot,
share_server=None):
"""Create a share from a snapshot - clone a snapshot."""
lcfg = self.configuration
LOG.debug("ZFSSAShareDriver.create_share_from_snapshot: clone=%s",
share['id'])
LOG.debug("ZFSSAShareDriver.create_share_from_snapshot: snapshot=%s",
snapshot['id'])
arg = self.create_arg(share['size'])
details = {
'share': share['id'],
'project': lcfg.zfssa_project,
}
arg.update(details)
if share['share_proto'].startswith('CIFS'):
arg.update({'sharesmb': 'on'})
self.zfssa.clone_snapshot(lcfg.zfssa_pool,
lcfg.zfssa_project,
snapshot,
share,
arg)
return self._export_location(share)
def delete_snapshot(self, context, snapshot, share_server=None):
"""Delete a snapshot.
Snapshots with existing clones cannot be deleted.
"""
LOG.debug("ZFSSAShareDriver.delete_snapshot: id=%s", snapshot['id'])
lcfg = self.configuration
has_clones = self.zfssa.has_clones(lcfg.zfssa_pool,
lcfg.zfssa_project,
snapshot['share_id'],
snapshot['id'])
if has_clones:
LOG.error(_LE("snapshot %s: has clones"), snapshot['id'])
raise exception.ShareSnapshotIsBusy(snapshot_name=snapshot['id'])
self.zfssa.delete_snapshot(lcfg.zfssa_pool,
lcfg.zfssa_project,
snapshot['share_id'],
snapshot['id'])
def ensure_share(self, context, share, share_server=None):
lcfg = self.configuration
details = self.zfssa.get_share(lcfg.zfssa_pool,
lcfg.zfssa_project,
share['id'])
if not details:
msg = (_("Share %s doesn't exists.") % share['id'])
raise exception.ManilaException(msg)
def allow_access(self, context, share, access, share_server=None):
"""Allows access to an NFS share for the specified IP."""
LOG.debug("ZFSSAShareDriver.allow_access: share=%s", share['id'])
lcfg = self.configuration
if share['share_proto'].startswith('NFS'):
self.zfssa.allow_access_nfs(lcfg.zfssa_pool,
lcfg.zfssa_project,
share['id'],
access)
def deny_access(self, context, share, access, share_server=None):
"""Deny access to an NFS share for the specified IP."""
LOG.debug("ZFSSAShareDriver.deny_access: share=%s", share['id'])
lcfg = self.configuration
if share['share_proto'].startswith('NFS'):
self.zfssa.deny_access_nfs(lcfg.zfssa_pool,
lcfg.zfssa_project,
share['id'],
access)
elif share['share_proto'].startswith('CIFS'):
return
def _update_share_status(self):
"""Retrieve status info from a share."""
LOG.debug("Updating share status...")
data = {}
backend_name = self.configuration.safe_get('share_backend_name')
data["share_backend_name"] = backend_name or self.__class__.__name__
data["vendor_name"] = 'Oracle'
data["driver_version"] = self.VERSION
data["storage_protocol"] = self.PROTOCOL
lcfg = self.configuration
(avail, used) = self.zfssa.get_pool_stats(lcfg.zfssa_pool)
if avail:
data['free_capacity_gb'] = int(avail) / units.Gi
if used:
total = int(avail) + int(used)
data['total_capacity_gb'] = total / units.Gi
else:
data['total_capacity_gb'] = 0
else:
data['free_capacity_gb'] = 0
data['total_capacity_gb'] = 0
data['reserved_percentage'] = 0
data['QoS_support'] = False
self._stats = data
def get_network_allocations_number(self):
"""Returns number of network allocations for creating VIFs."""
return 0

119
manila/tests/fake_zfssa.py Normal file
View File

@ -0,0 +1,119 @@
# Copyright (c) 2014, Oracle and/or its affiliates. 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.
"""
Fake ZFS Storage Appliance, for unit testing.
"""
class FakeResponse(object):
def __init__(self, statuscode):
self.status = statuscode
self.data = 'data'
class FakeZFSSA(object):
"""Fake ZFS SA."""
def __init__(self):
self.user = None
self.host = 'fakehost'
self.url = 'fakeurl'
self.rclient = None
def login(self, user):
self.user = user
def set_host(self, host, timeout=None):
self.host = host
def enable_service(self, service):
return True
def create_project(self, pool, project, arg):
pass
def get_share(self, pool, project, share):
pass
def create_share(self, pool, project, share):
pass
def delete_share(self, pool, project, share):
pass
def create_snapshot(self, pool, project, share):
pass
def delete_snapshot(self, pool, project, share, snapshot):
pass
def clone_snapshot(self, pool, project, share, snapshot, clone, size):
pass
def has_clones(self, pool, project, vol, snapshot):
return False
def modify_share(self, pool, project, share, arg):
pass
def allow_access_nfs(self, pool, project, share, access):
pass
def deny_access_nfs(self, pool, project, share, access):
pass
class FakeRestClient(object):
"""Fake ZFSSA Rest Client."""
def __init__(self):
self.url = None
self.headers = None
self.log_function = None
self.local = None
self.base_path = None
self.timeout = 60
self.do_logout = False
self.auth_str = None
def _path(self, path, base_path=None):
pass
def _authoriza(self):
pass
def login(self, auth_str):
pass
def logout(self):
pass
def islogin(self):
pass
def request(self, path, request, body=None, **kwargs):
pass
def get(self, path, **kwargs):
pass
def post(self, path, body="", **kwargs):
pass
def put(self, path, body="", **kwargs):
pass
def delete(self, path, **kwargs):
pass
def head(self, path, **kwargs):
pass

View File

@ -0,0 +1,396 @@
# Copyright (c) 2014, Oracle and/or its affiliates. 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.
"""
Unit tests for Oracle's ZFSSA REST API.
"""
import mock
from manila import exception
from manila.openstack.common import log
from manila.share.drivers.zfssa import restclient
from manila.share.drivers.zfssa import zfssarest
from manila import test
from manila.tests import fake_zfssa
LOG = log.getLogger(__name__)
class ZFSSAApiTestCase(test.TestCase):
"""Tests ZFSSAApi."""
@mock.patch.object(zfssarest, 'factory_restclient')
def setUp(self, _restclient):
super(ZFSSAApiTestCase, self).setUp()
self.host = 'fakehost'
self.user = 'fakeuser'
self.url = None
self.pool = 'fakepool'
self.project = 'fakeproject'
self.share = 'fakeshare'
self.snap = 'fakesnapshot'
_restclient.return_value = fake_zfssa.FakeRestClient()
self._zfssa = zfssarest.ZFSSAApi()
self._zfssa.set_host('fakehost')
def _create_response(self, status):
response = fake_zfssa.FakeResponse(status)
return response
def test_enable_service(self):
self.stubs.Set(self._zfssa.rclient, 'put', mock.Mock())
self._zfssa.rclient.put.return_value = self._create_response(
restclient.Status.ACCEPTED)
self._zfssa.enable_service('nfs')
self.assertEqual(1, self._zfssa.rclient.put.call_count)
self._zfssa.rclient.put.return_value = self._create_response(
restclient.Status.OK)
self.assertRaises(exception.ShareBackendException,
self._zfssa.enable_service,
'nfs')
def test_verify_avail_space(self):
self.stubs.Set(self._zfssa, 'verify_project', mock.Mock())
self.stubs.Set(self._zfssa, 'get_project_stats', mock.Mock())
self._zfssa.get_project_stats.return_value = 2000
self._zfssa.verify_avail_space(self.pool,
self.project,
self.share,
1000)
self.assertEqual(1, self._zfssa.verify_project.call_count)
self.assertEqual(1, self._zfssa.get_project_stats.call_count)
self._zfssa.verify_project.assert_called_with(self.pool, self.project)
self._zfssa.get_project_stats.assert_called_with(self.pool,
self.project)
self._zfssa.get_project_stats.return_value = 900
self.assertRaises(exception.ShareBackendException,
self._zfssa.verify_avail_space,
self.pool,
self.project,
self.share,
1000)
def test_create_project(self):
self.stubs.Set(self._zfssa, 'verify_pool', mock.Mock())
self.stubs.Set(self._zfssa.rclient, 'get', mock.Mock())
self.stubs.Set(self._zfssa.rclient, 'post', mock.Mock())
arg = {
'name': self.project,
'sharesmb': 'off',
'sharenfs': 'off',
'mountpoint': 'fakemnpt',
}
self._zfssa.rclient.get.return_value = self._create_response(
restclient.Status.NOT_FOUND)
self._zfssa.rclient.post.return_value = self._create_response(
restclient.Status.CREATED)
self._zfssa.create_project(self.pool, self.project, arg)
self.assertEqual(1, self._zfssa.rclient.get.call_count)
self.assertEqual(1, self._zfssa.rclient.post.call_count)
self.assertEqual(1, self._zfssa.verify_pool.call_count)
self._zfssa.verify_pool.assert_called_with(self.pool)
self._zfssa.rclient.post.return_value = self._create_response(
restclient.Status.NOT_FOUND)
self.assertRaises(exception.ShareBackendException,
self._zfssa.create_project,
self.pool,
self.project,
arg)
def test_create_share(self):
self.stubs.Set(self._zfssa, 'verify_avail_space', mock.Mock())
self.stubs.Set(self._zfssa.rclient, 'get', mock.Mock())
self.stubs.Set(self._zfssa.rclient, 'post', mock.Mock())
self._zfssa.rclient.get.return_value = self._create_response(
restclient.Status.NOT_FOUND)
self._zfssa.rclient.post.return_value = self._create_response(
restclient.Status.CREATED)
arg = {
"name": self.share,
"quota": 1,
}
self._zfssa.create_share(self.pool, self.project, arg)
self.assertEqual(1, self._zfssa.rclient.get.call_count)
self.assertEqual(1, self._zfssa.rclient.post.call_count)
self.assertEqual(1, self._zfssa.verify_avail_space.call_count)
self._zfssa.verify_avail_space.assert_called_with(self.pool,
self.project,
arg,
arg['quota'])
self._zfssa.rclient.post.return_value = self._create_response(
restclient.Status.NOT_FOUND)
self.assertRaises(exception.ShareBackendException,
self._zfssa.create_share,
self.pool,
self.project,
arg)
self._zfssa.rclient.get.return_value = self._create_response(
restclient.Status.OK)
self.assertRaises(exception.ShareBackendException,
self._zfssa.create_share,
self.pool,
self.project,
arg)
def test_modify_share(self):
self.stubs.Set(self._zfssa.rclient, 'put', mock.Mock())
self._zfssa.rclient.put.return_value = self._create_response(
restclient.Status.ACCEPTED)
arg = {"name": "dummyname"}
svc = self._zfssa.share_path % (self.pool, self.project, self.share)
self._zfssa.modify_share(self.pool, self.project, self.share, arg)
self.assertEqual(1, self._zfssa.rclient.put.call_count)
self._zfssa.rclient.put.assert_called_with(svc, arg)
self._zfssa.rclient.put.return_value = self._create_response(
restclient.Status.BAD_REQUEST)
self.assertRaises(exception.ShareBackendException,
self._zfssa.modify_share,
self.pool,
self.project,
self.share,
arg)
def test_delete_share(self):
self.stubs.Set(self._zfssa.rclient, 'delete', mock.Mock())
self._zfssa.rclient.delete.return_value = self._create_response(
restclient.Status.NO_CONTENT)
svc = self._zfssa.share_path % (self.pool, self.project, self.share)
self._zfssa.delete_share(self.pool, self.project, self.share)
self.assertEqual(1, self._zfssa.rclient.delete.call_count)
self._zfssa.rclient.delete.assert_called_with(svc)
def test_create_snapshot(self):
self.stubs.Set(self._zfssa.rclient, 'post', mock.Mock())
self._zfssa.rclient.post.return_value = self._create_response(
restclient.Status.CREATED)
arg = {"name": self.snap}
svc = self._zfssa.snapshots_path % (self.pool,
self.project,
self.share)
self._zfssa.create_snapshot(self.pool,
self.project,
self.share,
self.snap)
self.assertEqual(1, self._zfssa.rclient.post.call_count)
self._zfssa.rclient.post.assert_called_with(svc, arg)
self._zfssa.rclient.post.return_value = self._create_response(
restclient.Status.BAD_REQUEST)
self.assertRaises(exception.ShareBackendException,
self._zfssa.create_snapshot,
self.pool,
self.project,
self.share,
self.snap)
def test_delete_snapshot(self):
self.stubs.Set(self._zfssa.rclient, 'delete', mock.Mock())
self._zfssa.rclient.delete.return_value = self._create_response(
restclient.Status.NO_CONTENT)
svc = self._zfssa.snapshot_path % (self.pool,
self.project,
self.share,
self.snap)
self._zfssa.delete_snapshot(self.pool,
self.project,
self.share,
self.snap)
self.assertEqual(1, self._zfssa.rclient.delete.call_count)
self._zfssa.rclient.delete.assert_called_with(svc)
self._zfssa.rclient.delete.return_value = self._create_response(
restclient.Status.BAD_REQUEST)
self.assertRaises(exception.ShareBackendException,
self._zfssa.delete_snapshot,
self.pool,
self.project,
self.share,
self.snap)
def test_clone_snapshot(self):
self.stubs.Set(self._zfssa, 'verify_avail_space', mock.Mock())
self.stubs.Set(self._zfssa.rclient, 'put', mock.Mock())
self._zfssa.rclient.put.return_value = self._create_response(
restclient.Status.CREATED)
snapshot = {
"id": self.snap,
"share_id": self.share,
}
clone = {
"id": "cloneid",
"size": 1,
}
arg = {
"name": "dummyname",
"quota": 1,
}
self._zfssa.clone_snapshot(self.pool,
self.project,
snapshot,
clone,
arg)
self.assertEqual(1, self._zfssa.rclient.put.call_count)
self.assertEqual(1, self._zfssa.verify_avail_space.call_count)
self._zfssa.verify_avail_space.assert_called_with(self.pool,
self.project,
clone['id'],
clone['size'])
self._zfssa.rclient.put.return_value = self._create_response(
restclient.Status.NOT_FOUND)
self.assertRaises(exception.ShareBackendException,
self._zfssa.clone_snapshot,
self.pool,
self.project,
snapshot,
clone,
arg)
def _create_entry(self, sharenfs, ip):
if sharenfs == 'off':
sharenfs = 'sec=sys'
entry = (',rw=@%s' % ip)
if '/' not in ip:
entry = entry + '/32'
arg = {'sharenfs': sharenfs + entry}
return arg
def test_allow_access_nfs(self):
self.stubs.Set(self._zfssa, 'get_share', mock.Mock())
self.stubs.Set(self._zfssa, 'modify_share', mock.Mock())
details = {"sharenfs": "off"}
access = {
"access_type": "nonip",
"access_to": "foo",
}
# invalid access type
self.assertRaises(exception.InvalidShareAccess,
self._zfssa.allow_access_nfs,
self.pool,
self.project,
self.share,
access)
# valid entry
access.update({"access_type": "ip"})
arg = self._create_entry("off", access['access_to'])
self._zfssa.get_share.return_value = details
self._zfssa.allow_access_nfs(self.pool,
self.project,
self.share,
access)
self.assertEqual(1, self._zfssa.get_share.call_count)
self.assertEqual(1, self._zfssa.modify_share.call_count)
self._zfssa.get_share.assert_called_with(self.pool,
self.project,
self.share)
self._zfssa.modify_share.assert_called_with(self.pool,
self.project,
self.share,
arg)
# add another entry
access.update({"access_to": "10.0.0.1/24"})
arg = self._create_entry("off", access['access_to'])
self._zfssa.allow_access_nfs(self.pool,
self.project,
self.share,
access)
self.assertEqual(2, self._zfssa.modify_share.call_count)
self._zfssa.modify_share.assert_called_with(self.pool,
self.project,
self.share,
arg)
# verify modify_share is not called if sharenfs='on'
details = {"sharenfs": "on"}
self._zfssa.get_share.return_value = details
self._zfssa.allow_access_nfs(self.pool,
self.project,
self.share,
access)
self.assertEqual(2, self._zfssa.modify_share.call_count)
# verify modify_share is not called if ip is already in the list
access.update({"access_to": "10.0.0.1/24"})
details = self._create_entry("off", access['access_to'])
self._zfssa.get_share.return_value = details
self._zfssa.allow_access_nfs(self.pool,
self.project,
self.share,
access)
self.assertEqual(2, self._zfssa.modify_share.call_count)
def test_deny_access_nfs(self):
self.stubs.Set(self._zfssa, 'get_share', mock.Mock())
self.stubs.Set(self._zfssa, 'modify_share', mock.Mock())
data1 = self._create_entry("off", "10.0.0.1")
access = {
"access_type": "nonip",
"access_to": "foo",
}
# invalid access_type
self.assertRaises(exception.InvalidShareAccess,
self._zfssa.deny_access_nfs,
self.pool,
self.project,
self.share,
access)
# valid entry
access.update({"access_type": "ip"})
self._zfssa.get_share.return_value = data1
self._zfssa.deny_access_nfs(self.pool,
self.project,
self.share,
access)
self.assertEqual(1, self._zfssa.get_share.call_count)
self.assertEqual(0, self._zfssa.modify_share.call_count)
self._zfssa.get_share.assert_called_with(self.pool,
self.project,
self.share)
# another valid entry
data1 = self._create_entry(data1['sharenfs'], '10.0.0.2/24')
data2 = self._create_entry(data1['sharenfs'], access['access_to'])
self._zfssa.get_share.return_value = data2
self._zfssa.deny_access_nfs(self.pool,
self.project,
self.share,
access)
self.assertEqual(2, self._zfssa.get_share.call_count)
self.assertEqual(1, self._zfssa.modify_share.call_count)
self._zfssa.get_share.assert_called_with(self.pool,
self.project,
self.share)
self._zfssa.modify_share.assert_called_with(self.pool,
self.project,
self.share,
data1)

View File

@ -0,0 +1,211 @@
# Copyright (c) 2014, Oracle and/or its affiliates. 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.
"""
Unit tests for Oracle's ZFSSA Manila driver.
"""
import mock
from oslo.config import cfg
from manila import context
from manila import exception
from manila.share import configuration as conf
from manila.share.drivers.zfssa import zfssashare
from manila import test
from manila.tests import fake_zfssa
CONF = cfg.CONF
class ZFSSAShareDriverTestCase(test.TestCase):
"""Tests ZFSSAShareDriver."""
share = {
'id': 'fakeid',
'name': 'fakename',
'size': 1,
'share_proto': 'NFS',
'export_location': '127.0.0.1:/mnt/nfs/volume-00002',
}
snapshot = {
'id': 'fakesnapshotid',
'share_name': 'fakename',
'share_id': 'fakeid',
'name': 'fakesnapshotname',
'share_size': 1,
'share_proto': 'NFS',
'export_location': '127.0.0.1:/mnt/nfs/volume-00002',
}
access = {
'id': 'fakeaccid',
'access_type': 'ip',
'access_to': '10.0.0.2',
'state': 'active',
}
@mock.patch.object(zfssashare, 'factory_zfssa')
def setUp(self, _factory_zfssa):
super(ZFSSAShareDriverTestCase, self).setUp()
self._create_fake_config()
lcfg = self.configuration
self.mountpoint = '/export/' + lcfg.zfssa_nas_mountpoint
_factory_zfssa.return_value = fake_zfssa.FakeZFSSA()
_factory_zfssa.set_host(lcfg.zfssa_host)
_factory_zfssa.login(lcfg.zfssa_auth_user)
self._context = context.get_admin_context()
self._driver = zfssashare.ZFSSAShareDriver(configuration=lcfg)
self._driver.do_setup(self._context)
def _create_fake_config(self):
self.configuration = mock.Mock(spec=conf.Configuration)
self.configuration.zfssa_host = '1.1.1.1'
self.configuration.zfssa_data_ip = '1.1.1.1'
self.configuration.zfssa_auth_user = 'user'
self.configuration.zfssa_auth_password = 'passwd'
self.configuration.zfssa_pool = 'pool'
self.configuration.zfssa_project = 'project'
self.configuration.zfssa_nas_mountpoint = 'project'
self.configuration.zfssa_nas_checksum = 'fletcher4'
self.configuration.zfssa_nas_logbias = 'latency'
self.configuration.zfssa_nas_compression = 'off'
self.configuration.zfssa_nas_vscan = 'false'
self.configuration.zfssa_nas_rstchown = 'true'
self.configuration.zfssa_nas_quota_snap = 'true'
self.configuration.zfssa_rest_timeout = 60
self.configuration.network_config_group = 'fake_network_config_group'
def test_create_share(self):
self.stubs.Set(self._driver.zfssa, 'create_share', mock.Mock())
self.stubs.Set(self._driver, '_export_location', mock.Mock())
lcfg = self.configuration
arg = {
'host': lcfg.zfssa_data_ip,
'mountpoint': self.mountpoint,
'name': self.share['id'],
}
location = ("%(host)s:%(mountpoint)s/%(name)s" % arg)
self._driver._export_location.return_value = location
arg = self._driver.create_arg(self.share['size'])
arg.update(self._driver.default_args)
arg.update({'name': self.share['id']})
ret = self._driver.create_share(self._context, self.share)
self._driver.zfssa.create_share.assert_called_with(lcfg.zfssa_pool,
lcfg.zfssa_project,
arg)
self.assertEqual(location, ret)
self.assertEqual(1, self._driver.zfssa.create_share.call_count)
self.assertEqual(1, self._driver._export_location.call_count)
def test_create_share_from_snapshot(self):
self.stubs.Set(self._driver.zfssa, 'clone_snapshot', mock.Mock())
self.stubs.Set(self._driver, '_export_location', mock.Mock())
lcfg = self.configuration
arg = {
'host': lcfg.zfssa_data_ip,
'mountpoint': self.mountpoint,
'name': self.share['id'],
}
location = ("%(host)s:%(mountpoint)s/%(name)s" % arg)
self._driver._export_location.return_value = location
arg = self._driver.create_arg(self.share['size'])
details = {
'share': self.share['id'],
'project': lcfg.zfssa_project,
}
arg.update(details)
ret = self._driver.create_share_from_snapshot(self._context,
self.share,
self.snapshot)
self.assertEqual(location, ret)
self.assertEqual(1, self._driver.zfssa.clone_snapshot.call_count)
self.assertEqual(1, self._driver._export_location.call_count)
self._driver.zfssa.clone_snapshot.assert_called_with(
lcfg.zfssa_pool,
lcfg.zfssa_project,
self.snapshot,
self.share,
arg)
def test_delete_share(self):
self.stubs.Set(self._driver.zfssa, 'delete_share', mock.Mock())
self._driver.delete_share(self._context, self.share)
self.assertEqual(1, self._driver.zfssa.delete_share.call_count)
lcfg = self.configuration
self._driver.zfssa.delete_share.assert_called_with(lcfg.zfssa_pool,
lcfg.zfssa_project,
self.share['id'])
def test_create_snapshot(self):
self.stubs.Set(self._driver.zfssa, 'create_snapshot', mock.Mock())
lcfg = self.configuration
self._driver.create_snapshot(self._context, self.snapshot)
self.assertEqual(1, self._driver.zfssa.create_snapshot.call_count)
self._driver.zfssa.create_snapshot.assert_called_with(
lcfg.zfssa_pool,
lcfg.zfssa_project,
self.snapshot['share_id'],
self.snapshot['id'])
def test_delete_snapshot(self):
self.stubs.Set(self._driver.zfssa, 'delete_snapshot', mock.Mock())
self._driver.delete_snapshot(self._context, self.snapshot)
self.assertEqual(1, self._driver.zfssa.delete_snapshot.call_count)
def test_delete_snapshot_negative(self):
self.stubs.Set(self._driver.zfssa, 'has_clones', mock.Mock())
self._driver.zfssa.has_clones.return_value = True
self.assertRaises(exception.ShareSnapshotIsBusy,
self._driver.delete_snapshot,
self._context,
self.snapshot)
def test_ensure_share(self):
self.stubs.Set(self._driver.zfssa, 'get_share', mock.Mock())
lcfg = self.configuration
self._driver.ensure_share(self._context, self.share)
self.assertEqual(1, self._driver.zfssa.get_share.call_count)
self._driver.zfssa.get_share.assert_called_with(
lcfg.zfssa_pool,
lcfg.zfssa_project,
self.share['id'])
self._driver.zfssa.get_share.return_value = None
self.assertRaises(exception.ManilaException,
self._driver.ensure_share,
self._context,
self.share)
def test_allow_access(self):
self.stubs.Set(self._driver.zfssa, 'allow_access_nfs', mock.Mock())
lcfg = self.configuration
self._driver.allow_access(self._context, self.share, self.access)
self.assertEqual(1, self._driver.zfssa.allow_access_nfs.call_count)
self._driver.zfssa.allow_access_nfs.assert_called_with(
lcfg.zfssa_pool,
lcfg.zfssa_project,
self.share['id'],
self.access)
def test_deny_access(self):
self.stubs.Set(self._driver.zfssa, 'deny_access_nfs', mock.Mock())
lcfg = self.configuration
self._driver.deny_access(self._context, self.share, self.access)
self.assertEqual(1, self._driver.zfssa.deny_access_nfs.call_count)
self._driver.zfssa.deny_access_nfs.assert_called_with(
lcfg.zfssa_pool,
lcfg.zfssa_project,
self.share['id'],
self.access)