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:
parent
28f311c9ad
commit
6c4f6f6340
@ -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,
|
||||
|
0
manila/share/drivers/zfssa/__init__.py
Normal file
0
manila/share/drivers/zfssa/__init__.py
Normal file
369
manila/share/drivers/zfssa/restclient.py
Normal file
369
manila/share/drivers/zfssa/restclient.py
Normal 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)
|
387
manila/share/drivers/zfssa/zfssarest.py
Normal file
387
manila/share/drivers/zfssa/zfssarest.py
Normal 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)
|
331
manila/share/drivers/zfssa/zfssashare.py
Normal file
331
manila/share/drivers/zfssa/zfssashare.py
Normal 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
119
manila/tests/fake_zfssa.py
Normal 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
|
0
manila/tests/share/drivers/zfssa/__init__.py
Normal file
0
manila/tests/share/drivers/zfssa/__init__.py
Normal file
396
manila/tests/share/drivers/zfssa/test_zfssarest.py
Normal file
396
manila/tests/share/drivers/zfssa/test_zfssarest.py
Normal 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)
|
211
manila/tests/share/drivers/zfssa/test_zfssashare.py
Normal file
211
manila/tests/share/drivers/zfssa/test_zfssashare.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user