Browse Source

ganesha: NFS-Ganesha instrumentation

Introduce the ganesha share driver helper module
which provides the GaneshaNASHelper class from which
share drivers can derive NFS-Ganesha backed protocol
helpers.

Some utility functions are also added to ease
integration.

Partially implements blueprint gateway-mediated-with-ganesha

Change-Id: I8683ea5eb43d7a8eaf0dfa6af3791782d32b944a
Csaba Henk 4 years ago
parent
commit
559b478e85

+ 17
- 0
etc/manila/rootwrap.d/share.filters View File

@@ -3,6 +3,7 @@
3 3
 
4 4
 [Filters]
5 5
 # manila/share/drivers/glusterfs.py: 'mkdir', '%s'
6
+# manila/share/drivers/ganesha/manager.py: 'mkdir', '-p', '%s'
6 7
 mkdir: CommandFilter, /usr/bin/mkdir, root
7 8
 
8 9
 # manila/share/drivers/glusterfs.py: 'rm', '-rf', '%s'
@@ -49,6 +50,7 @@ rsync: CommandFilter, /usr/bin/rsync, root
49 50
 exportfs: CommandFilter, /usr/sbin/exportfs, root
50 51
 # Ganesha commands
51 52
 # manila/share/drivers/ibm/ganesha_utils.py: 'mv', '%s', '%s'
53
+# manila/share/drivers/ganesha/manager.py: 'mv', '%s', '%s'
52 54
 mv: CommandFilter, /bin/mv, root
53 55
 # manila/share/drivers/ibm/ganesha_utils.py: 'cp', '%s', '%s'
54 56
 cp: CommandFilter, /bin/cp, root
@@ -60,3 +62,18 @@ ssh: CommandFilter, /usr/bin/ssh, root
60 62
 chmod: CommandFilter, /bin/chmod, root
61 63
 # manila/share/drivers/ibm/ganesha_utils.py: 'service', '%s', 'restart'
62 64
 service: CommandFilter, /sbin/service, root
65
+
66
+# manila/share/drivers/ganesha/manager.py: 'mktemp', '-p', '%s', '-t', '%s'
67
+mktemp: CommandFilter, /bin/mktemp, root
68
+
69
+# manila/share/drivers/ganesha/manager.py:
70
+shcat: RegExpFilter, /bin/sh, root, sh, -c, cat > /.*
71
+
72
+# manila/share/drivers/ganesha/manager.py:
73
+dbus-addexport: RegExpFilter, /usr/bin/dbus-send, root, dbus-send, --print-reply, --system, --dest=org\.ganesha\.nfsd, /org/ganesha/nfsd/ExportMgr, org\.ganesha\.nfsd\.exportmgr\.(Add|Remove)Export, .*, .*
74
+
75
+# manila/share/drivers/ganesha/manager.py:
76
+dbus-removeexport: RegExpFilter, /usr/bin/dbus-send, root, dbus-send, --print-reply, --system, --dest=org\.ganesha\.nfsd, /org/ganesha/nfsd/ExportMgr, org\.ganesha\.nfsd\.exportmgr\.(Add|Remove)Export, .*
77
+
78
+# manila/share/drivers/ganesha/manager.py:
79
+rmconf: RegExpFilter, /bin/sh, root, sh, -c, rm /.*/\*\.conf$

+ 13
- 0
manila/exception.py View File

@@ -464,3 +464,16 @@ class GPFSException(ManilaException):
464 464
 
465 465
 class GPFSGaneshaException(ManilaException):
466 466
     message = _("GPFS Ganesha exception occurred.")
467
+
468
+
469
+class GaneshaCommandFailure(ProcessExecutionError):
470
+    _description = _("Ganesha management command failed.")
471
+
472
+    def __init__(self, **kw):
473
+        if 'description' not in kw:
474
+            kw['description'] = self._description
475
+        super(GaneshaCommandFailure, self).__init__(**kw)
476
+
477
+
478
+class InvalidSqliteDB(Invalid):
479
+    message = _("Invalid Sqlite database.")

+ 1
- 0
manila/opts.py View File

@@ -95,6 +95,7 @@ _global_opt_lists = [
95 95
     manila.scheduler.weights.capacity.capacity_weight_opts,
96 96
     manila.service.service_opts,
97 97
     manila.share.api.share_api_opts,
98
+    manila.share.driver.ganesha_opts,
98 99
     manila.share.driver.share_opts,
99 100
     manila.share.driver.ssh_opts,
100 101
     manila.share.drivers.emc.driver.EMC_NAS_OPTS,

+ 42
- 0
manila/share/driver.py View File

@@ -77,9 +77,40 @@ ssh_opts = [
77 77
         help='Maximum number of connections in the SSH pool.'),
78 78
 ]
79 79
 
80
+ganesha_opts = [
81
+    cfg.StrOpt('ganesha_config_dir',
82
+               default='/etc/ganesha',
83
+               help='Directory where Ganesha config files are stored.'),
84
+    cfg.StrOpt('ganesha_config_path',
85
+               default='$ganesha_config_dir/ganesha.conf',
86
+               help='Path to main Ganesha config file.'),
87
+    cfg.StrOpt('ganesha_nfs_export_options',
88
+               default='maxread = 65536, prefread = 65536',
89
+               help='Options to use when exporting a share using ganesha '
90
+                    'NFS server. Note that these defaults can be overridden '
91
+                    'when a share is created by passing metadata with key '
92
+                    'name export_options.  Also note the complete set of '
93
+                    'default ganesha export options is specified in '
94
+                    'ganesha_utils. (GPFS only.)'),
95
+    cfg.StrOpt('ganesha_service_name',
96
+               default='ganesha.nfsd',
97
+               help='Name of the ganesha nfs service.'),
98
+    cfg.StrOpt('ganesha_db_path',
99
+               default='$state_path/manila-ganesha.db',
100
+               help='Location of Ganesha database file. '
101
+                    '(Ganesha module only.)'),
102
+    cfg.StrOpt('ganesha_export_dir',
103
+               default='$ganesha_config_dir/export.d',
104
+               help='Path to Ganesha export template. (Ganesha module only.)'),
105
+    cfg.StrOpt('ganesha_export_template_dir',
106
+               default='/etc/manila/ganesha-export-templ.d',
107
+               help='Path to Ganesha export template. (Ganesha module only.)'),
108
+]
109
+
80 110
 CONF = cfg.CONF
81 111
 CONF.register_opts(share_opts)
82 112
 CONF.register_opts(ssh_opts)
113
+CONF.register_opts(ganesha_opts)
83 114
 
84 115
 
85 116
 class ExecuteMixin(object):
@@ -111,6 +142,14 @@ class ExecuteMixin(object):
111 142
                 time.sleep(tries ** 2)
112 143
 
113 144
 
145
+class GaneshaMixin(object):
146
+    """Augment derived classes with Ganesha configuration."""
147
+
148
+    def init_ganesha_mixin(self, *args, **kwargs):
149
+        if self.configuration:
150
+            self.configuration.append_config_values(ganesha_opts)
151
+
152
+
114 153
 class ShareDriver(object):
115 154
     """Class defines interface of NAS driver."""
116 155
 
@@ -129,6 +168,9 @@ class ShareDriver(object):
129 168
         if hasattr(self, 'init_execute_mixin'):
130 169
             # Instance with 'ExecuteMixin'
131 170
             self.init_execute_mixin(*args, **kwargs)  # pylint: disable=E1101
171
+        if hasattr(self, 'init_ganesha_mixin'):
172
+            # Instance with 'GaneshaMixin'
173
+            self.init_execute_mixin(*args, **kwargs)  # pylint: disable=E1101
132 174
         self.network_api = network.API(config_group_name=network_config_group)
133 175
 
134 176
     def _validate_driver_mode(self, mode):

+ 141
- 0
manila/share/drivers/ganesha/__init__.py View File

@@ -0,0 +1,141 @@
1
+# Copyright (c) 2014 Red Hat, Inc.
2
+# All Rights Reserved.
3
+#
4
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+#    not use this file except in compliance with the License. You may obtain
6
+#    a copy of the License at
7
+#
8
+#         http://www.apache.org/licenses/LICENSE-2.0
9
+#
10
+#    Unless required by applicable law or agreed to in writing, software
11
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+#    License for the specific language governing permissions and limitations
14
+#    under the License.
15
+
16
+import abc
17
+import errno
18
+import os
19
+import re
20
+
21
+from oslo.config import cfg
22
+import six
23
+
24
+from manila import exception
25
+from manila.i18n import _LI
26
+from manila.openstack.common import log as logging
27
+from manila.share.drivers.ganesha import manager as ganesha_manager
28
+from manila.share.drivers.ganesha import utils as ganesha_utils
29
+
30
+
31
+CONF = cfg.CONF
32
+LOG = logging.getLogger(__name__)
33
+
34
+
35
+@six.add_metaclass(abc.ABCMeta)
36
+class NASHelperBase(object):
37
+    """Interface to work with share."""
38
+
39
+    def __init__(self, execute, config, **kwargs):
40
+        self.configuration = config
41
+        self._execute = execute
42
+
43
+    def init_helper(self):
44
+        """Initializes protocol-specific NAS drivers."""
45
+
46
+    @abc.abstractmethod
47
+    def allow_access(self, base_path, share, access):
48
+        """Allow access to the host."""
49
+
50
+    @abc.abstractmethod
51
+    def deny_access(self, base_path, share, access):
52
+        """Deny access to the host."""
53
+
54
+
55
+class GaneshaNASHelper(NASHelperBase):
56
+    """Execute commands relating to Shares."""
57
+
58
+    def __init__(self, execute, config, tag='<no name>', **kwargs):
59
+        super(GaneshaNASHelper, self).__init__(execute, config, **kwargs)
60
+        self.tag = tag
61
+
62
+    confrx = re.compile('\.(conf|json)\Z')
63
+
64
+    def _load_conf_dir(self, dirpath, must_exist=True):
65
+        """Load Ganesha config files in dirpath in alphabetic order."""
66
+        try:
67
+            dirlist = os.listdir(dirpath)
68
+        except OSError as e:
69
+            if e.errno != errno.ENOENT or must_exist:
70
+                raise
71
+            dirlist = []
72
+        LOG.info(_LI('Loading Ganesha config from %s.'), dirpath)
73
+        conf_files = filter(self.confrx.search, dirlist)
74
+        conf_files.sort()
75
+        export_template = {}
76
+        for conf_file in conf_files:
77
+            with open(os.path.join(dirpath, conf_file)) as f:
78
+                ganesha_utils.patch(
79
+                    export_template,
80
+                    ganesha_manager.parseconf(f.read()))
81
+        return export_template
82
+
83
+    def init_helper(self):
84
+        """Initializes protocol-specific NAS drivers."""
85
+        self.ganesha = ganesha_manager.GaneshaManager(
86
+            self._execute,
87
+            self.tag,
88
+            ganesha_config_path=self.configuration.ganesha_config_path,
89
+            ganesha_export_dir=self.configuration.ganesha_export_dir,
90
+            ganesha_db_path=self.configuration.ganesha_db_path,
91
+            ganesha_service_name=self.configuration.ganesha_service_name)
92
+        system_export_template = self._load_conf_dir(
93
+            self.configuration.ganesha_export_template_dir,
94
+            must_exist=False)
95
+        if system_export_template:
96
+            self.export_template = system_export_template
97
+        else:
98
+            self.export_template = self._default_config_hook()
99
+
100
+    def _default_config_hook(self):
101
+        """The default export block.
102
+
103
+        Subclass this to add FSAL specific defaults.
104
+
105
+        Suggested approach: take the return value of superclass'
106
+        method, patch with dict containing your defaults, and
107
+        return the result. However, you can also provide your
108
+        defaults from scratch with no regard to superclass.
109
+        """
110
+
111
+        return self._load_conf_dir(ganesha_utils.path_from(__file__, "conf"))
112
+
113
+    def _fsal_hook(self, base_path, share, access):
114
+        """Subclass this to create FSAL block."""
115
+        return {}
116
+
117
+    def allow_access(self, base_path, share, access):
118
+        """Allow access to the share."""
119
+        if access['access_type'] != 'ip':
120
+            raise exception.InvalidShareAccess('Only IP access type allowed')
121
+        cf = {}
122
+        accid = access['id']
123
+        name = share['name']
124
+        export_name = "%s--%s" % (name, accid)
125
+        ganesha_utils.patch(cf, self.export_template, {
126
+            'EXPORT': {
127
+                'Export_Id': self.ganesha.get_export_id(),
128
+                'Path': os.path.join(base_path, name),
129
+                'Pseudo': os.path.join(base_path, export_name),
130
+                'Tag': accid,
131
+                'CLIENT': {
132
+                    'Clients': access['access_to']
133
+                },
134
+                'FSAL': self._fsal_hook(base_path, share, access)
135
+            }
136
+        })
137
+        self.ganesha.add_export(export_name, cf)
138
+
139
+    def deny_access(self, base_path, share, access):
140
+        """Deny access to the share."""
141
+        self.ganesha.remove_export("%s--%s" % (share['name'], access['id']))

+ 48
- 0
manila/share/drivers/ganesha/conf/00-base-export-template.conf View File

@@ -0,0 +1,48 @@
1
+# This is a Ganesha config template.
2
+# Syntactically, a valid Ganesha config
3
+# file, but some values in it are stubs.
4
+# Fields that have stub values are managed
5
+# by Manila; the stubs are of two kinds:
6
+# - @config:
7
+#    value will be taken from Manila config
8
+# - @runtime:
9
+#    value will be determined at runtime
10
+# User is free to set Ganesha parameters
11
+# which are not reserved to Manila by
12
+# stubbing.
13
+
14
+EXPORT {
15
+    # Each EXPORT must have a unique Export_Id.
16
+    Export_Id = @runtime;
17
+
18
+    # The directory in the exported file system this export
19
+    # is rooted on.
20
+    Path = @runtime;
21
+
22
+    # FSAL, Ganesha's module component
23
+    FSAL {
24
+        # FSAL name
25
+        Name = @config;
26
+    }
27
+
28
+    # Path of export in the NFSv4 pseudo filesystem
29
+    Pseudo = @runtime;
30
+
31
+    # RPC security flavor, one of none, sys, krb5{,i,p}
32
+    SecType = sys;
33
+
34
+    # Alternative export identifier for NFSv3
35
+    Tag = @runtime;
36
+
37
+    # Client specification
38
+    CLIENT {
39
+        # Comma separated list of clients
40
+        Clients = @runtime;
41
+
42
+	# Access type, one of RW, RO, MDONLY, MDONLY_RO, NONE
43
+        Access_Type = RW;
44
+    }
45
+
46
+    # User id squashing, one of None, Root, All
47
+    Squash = None;
48
+}

+ 344
- 0
manila/share/drivers/ganesha/manager.py View File

@@ -0,0 +1,344 @@
1
+# Copyright (c) 2014 Red Hat, Inc.
2
+# All Rights Reserved.
3
+#
4
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+#    not use this file except in compliance with the License. You may obtain
6
+#    a copy of the License at
7
+#
8
+#         http://www.apache.org/licenses/LICENSE-2.0
9
+#
10
+#    Unless required by applicable law or agreed to in writing, software
11
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+#    License for the specific language governing permissions and limitations
14
+#    under the License.
15
+
16
+import os
17
+import pipes
18
+import re
19
+import sys
20
+
21
+from oslo.serialization import jsonutils
22
+import six
23
+
24
+from manila import exception
25
+from manila.i18n import _
26
+from manila.i18n import _LE
27
+from manila.openstack.common import log as logging
28
+from manila.share.drivers.ganesha import utils as ganesha_utils
29
+from manila import utils
30
+
31
+
32
+LOG = logging.getLogger(__name__)
33
+IWIDTH = 4
34
+
35
+
36
+def _conf2json(conf):
37
+    """Convert Ganesha config to JSON."""
38
+
39
+    # tokenize config string
40
+    token_list = [six.StringIO()]
41
+    state = {
42
+        'in_quote': False,
43
+        'in_comment': False,
44
+        'escape': False,
45
+    }
46
+
47
+    cbk = []
48
+    for char in conf:
49
+        if state['in_quote']:
50
+            if not state['escape']:
51
+                if char == '"':
52
+                    state['in_quote'] = False
53
+                    cbk.append(lambda: token_list.append(six.StringIO()))
54
+                elif char == '\\':
55
+                    cbk.append(lambda: state.update({'escape': True}))
56
+        else:
57
+            if char == "#":
58
+                state['in_comment'] = True
59
+            if state['in_comment']:
60
+                if char == "\n":
61
+                    state['in_comment'] = False
62
+            else:
63
+                if char == '"':
64
+                    token_list.append(six.StringIO())
65
+                    state['in_quote'] = True
66
+        state['escape'] = False
67
+        if not state['in_comment']:
68
+            token_list[-1].write(char)
69
+        while cbk:
70
+            cbk.pop(0)()
71
+
72
+    if state['in_quote']:
73
+        raise RuntimeError("Unterminated quoted string")
74
+
75
+    # jsonify tokens
76
+    js_token_list = ["{"]
77
+    for tok in token_list:
78
+        tok = tok.getvalue()
79
+
80
+        if tok[0] == '"':
81
+            js_token_list.append(tok)
82
+            continue
83
+
84
+        for pat, s in [
85
+                # add omitted "=" signs to block openings
86
+                ('([^=\s])\s*{', '\\1={'),
87
+                # delete trailing semicolons in blocks
88
+                (';\s*}', '}'),
89
+                # add omitted semicolons after blocks
90
+                ('}\s*([^}\s])', '};\\1'),
91
+                # separate syntactically significant characters
92
+                ('([;{}=])', ' \\1 ')]:
93
+            tok = re.sub(pat, s, tok)
94
+
95
+        # map tokens to JSON equivalents
96
+        for word in tok.split():
97
+            if word == "=":
98
+                word = ":"
99
+            elif word == ";":
100
+                word = ','
101
+            elif (word in ['{', '}'] or
102
+                  re.search('\A-?[1-9]\d*(\.\d+)?\Z', word)):
103
+                pass
104
+            else:
105
+                word = jsonutils.dumps(word)
106
+            js_token_list.append(word)
107
+    js_token_list.append("}")
108
+
109
+    # group quouted strings
110
+    token_grp_list = []
111
+    for tok in js_token_list:
112
+        if tok[0] == '"':
113
+            if not (token_grp_list and isinstance(token_grp_list[-1], list)):
114
+                token_grp_list.append([])
115
+            token_grp_list[-1].append(tok)
116
+        else:
117
+            token_grp_list.append(tok)
118
+
119
+    # process quoted string groups by joining them
120
+    js_token_list2 = []
121
+    for x in token_grp_list:
122
+        if isinstance(x, list):
123
+            x = ''.join(['"'] + [tok[1:-1] for tok in x] + ['"'])
124
+        js_token_list2.append(x)
125
+
126
+    return ''.join(js_token_list2)
127
+
128
+
129
+def _dump_to_conf(confdict, out=sys.stdout, indent=0):
130
+    """Output confdict in Ganesha config format."""
131
+    if isinstance(confdict, dict):
132
+        for k, v in six.iteritems(confdict):
133
+            if v is None:
134
+                continue
135
+            out.write(' ' * (indent * IWIDTH) + k + ' ')
136
+            if isinstance(v, dict):
137
+                out.write("{\n")
138
+                _dump_to_conf(v, out, indent + 1)
139
+                out.write(' ' * (indent * IWIDTH) + '}')
140
+            else:
141
+                out.write('= ')
142
+                _dump_to_conf(v, out, indent)
143
+                out.write(';')
144
+            out.write('\n')
145
+    else:
146
+        dj = jsonutils.dumps(confdict)
147
+        if confdict == dj[1:-1]:
148
+            out.write(confdict)
149
+        else:
150
+            out.write(dj)
151
+
152
+
153
+def parseconf(conf):
154
+    """Parse Ganesha config.
155
+
156
+    Both native format and JSON are supported.
157
+    """
158
+
159
+    try:
160
+        # allow config to be specified in JSON --
161
+        # for sake of people who might feel Ganesha config foreign.
162
+        d = jsonutils.loads(conf)
163
+    except ValueError:
164
+        d = jsonutils.loads(_conf2json(conf))
165
+    return d
166
+
167
+
168
+def mkconf(confdict):
169
+    """Create Ganesha config string from confdict."""
170
+    s = six.StringIO()
171
+    _dump_to_conf(confdict, s)
172
+    return s.getvalue()
173
+
174
+
175
+class GaneshaManager(object):
176
+    """Ganesha instrumentation class."""
177
+
178
+    def __init__(self, execute, tag, **kwargs):
179
+        self.confrx = re.compile('\.conf\Z')
180
+        self.ganesha_config_path = kwargs['ganesha_config_path']
181
+        self.tag = tag
182
+
183
+        def _execute(*args, **kwargs):
184
+            msg = kwargs.pop('message', args[0])
185
+            makelog = kwargs.pop('makelog', True)
186
+            try:
187
+                return execute(*args, **kwargs)
188
+            except exception.ProcessExecutionError as e:
189
+                if makelog:
190
+                    LOG.error(
191
+                        _LE("Error while executing management command on "
192
+                            "Ganesha node %(tag)s: %(msg)s."),
193
+                        {'tag': tag, 'msg': msg})
194
+                raise exception.GaneshaCommandFailure(
195
+                    stdout=e.stdout, stderr=e.stderr, exit_code=e.exit_code,
196
+                    cmd=e.cmd)
197
+        self.execute = _execute
198
+        self.ganesha_export_dir = kwargs['ganesha_export_dir']
199
+        self.execute('mkdir', '-p', self.ganesha_export_dir)
200
+        self.ganesha_db_path = kwargs['ganesha_db_path']
201
+        self.execute('mkdir', '-p', os.path.dirname(self.ganesha_db_path))
202
+        self.ganesha_service = kwargs['ganesha_service_name']
203
+        # Here we are to make sure that an SQLite database of the
204
+        # required scheme exists at self.ganesha_db_path.
205
+        # The following command gets us there -- provided the file
206
+        # does not yet exist (otherwise it just fails). However,
207
+        # we don't care about this condition, we just execute the
208
+        # command unconditionally (ignoring failure). Instead we
209
+        # directly query the db right after, to check its validity.
210
+        self.execute("sqlite3", self.ganesha_db_path,
211
+                     'create table ganesha(key varchar(20) primary key, '
212
+                     'value int); insert into ganesha values("exportid", '
213
+                     '100);', run_as_root=False, check_exit_code=False)
214
+        self.get_export_id(bump=False)
215
+        # Starting from empty state. State will be rebuilt in a later
216
+        # stage of service initalization.
217
+        self.reset_exports()
218
+        self.restart_service()
219
+
220
+    def _getpath(self, name):
221
+        """Get the path of config file for name."""
222
+        return os.path.join(self.ganesha_export_dir, name + ".conf")
223
+
224
+    def _write_file(self, path, data):
225
+        """Write data to path atomically."""
226
+        dirpath, fname = (getattr(os.path, q + "name")(path) for q in
227
+                          ("dir", "base"))
228
+        tmpf = self.execute('mktemp', '-p', dirpath, "-t",
229
+                            fname + ".XXXXXX")[0][:-1]
230
+        self.execute('sh', '-c', 'cat > ' + pipes.quote(tmpf),
231
+                     process_input=data, message='writing ' + tmpf)
232
+        self.execute('mv', tmpf, path)
233
+
234
+    def _write_conf_file(self, name, data):
235
+        """Write data to config file for name atomically."""
236
+        path = self._getpath(name)
237
+        self._write_file(path, data)
238
+        return path
239
+
240
+    def _mkindex(self):
241
+        """Generate the index file for current exports."""
242
+        @utils.synchronized("ganesha-index-" + self.tag, external=True)
243
+        def _mkindex():
244
+            files = filter(lambda f: self.confrx.search(f) and
245
+                           f != "INDEX.conf",
246
+                           self.execute('ls', self.ganesha_export_dir,
247
+                                        run_as_root=False)[0].split("\n"))
248
+            index = "".join(map(lambda f: "%include " + os.path.join(
249
+                    self.ganesha_export_dir, f) + "\n", files))
250
+            self._write_conf_file("INDEX", index)
251
+        _mkindex()
252
+
253
+    def _read_export_file(self, name):
254
+        """Return the dict of the export identified by name."""
255
+        return parseconf(self.execute("cat", self._getpath(name),
256
+                                      message='reading export ' + name)[0])
257
+
258
+    def _write_export_file(self, name, confdict):
259
+        """Write confdict to the export file of name."""
260
+        for k, v in ganesha_utils.walk(confdict):
261
+            # values in the export block template that need to be
262
+            # filled in by Manila are pre-fixed by '@'
263
+            if isinstance(v, basestring) and v[0] == '@':
264
+                msg = _("Incomplete export block: value %(val)s of attribute "
265
+                        "%(key)s is a stub.") % {'key': k, 'val': v}
266
+                raise exception.InvalidParameterValue(err=msg)
267
+        return self._write_conf_file(name, mkconf(confdict))
268
+
269
+    def _rm_export_file(self, name):
270
+        """Remove export file of name."""
271
+        self.execute("rm", self._getpath(name))
272
+
273
+    def _dbus_send_ganesha(self, method, *args, **kwargs):
274
+        """Send a message to Ganesha via dbus."""
275
+        service = kwargs.pop("service", "exportmgr")
276
+        self.execute("dbus-send", "--print-reply", "--system",
277
+                     "--dest=org.ganesha.nfsd", "/org/ganesha/nfsd/ExportMgr",
278
+                     "org.ganesha.nfsd.%s.%s" % (service, method), *args,
279
+                     message='dbus call %s.%s' % (service, method), **kwargs)
280
+
281
+    def _remove_export_dbus(self, xid):
282
+        """Remove an export from Ganesha runtime with given export id."""
283
+        self._dbus_send_ganesha("RemoveExport", "uint16:%d" % xid)
284
+
285
+    def add_export(self, name, confdict):
286
+        """Add an export to Ganesha specified by confdict."""
287
+        xid = confdict["EXPORT"]["Export_Id"]
288
+        undos = []
289
+        _mkindex_called = False
290
+        try:
291
+            path = self._write_export_file(name, confdict)
292
+            undos.append(lambda: self._rm_export_file(name))
293
+
294
+            self._dbus_send_ganesha("AddExport", "string:" + path,
295
+                                    "string:EXPORT(Export_Id=%d)" % xid)
296
+            undos.append(lambda: self._remove_export_dbus(xid))
297
+
298
+            _mkindex_called = True
299
+            self._mkindex()
300
+        except Exception:
301
+            for u in undos:
302
+                u()
303
+            if not _mkindex_called:
304
+                self._mkindex()
305
+            raise
306
+
307
+    def remove_export(self, name):
308
+        """Remove an export from Ganesha."""
309
+        try:
310
+            confdict = self._read_export_file(name)
311
+            self._remove_export_dbus(confdict["EXPORT"]["Export_Id"])
312
+        finally:
313
+            self._rm_export_file(name)
314
+            self._mkindex()
315
+
316
+    def get_export_id(self, bump=True):
317
+        """Get a new export id."""
318
+        # XXX overflowing the export id (16 bit unsigned integer)
319
+        # is not handled
320
+        if bump:
321
+            bumpcode = 'update ganesha set value = value + 1;'
322
+        else:
323
+            bumpcode = ''
324
+        out = self.execute(
325
+            "sqlite3", self.ganesha_db_path,
326
+            bumpcode + 'select * from ganesha where key = "exportid";',
327
+            run_as_root=False)[0]
328
+        match = re.search('\Aexportid\|(\d+)$', out)
329
+        if not match:
330
+            LOG.error(_LE("Invalid export database on "
331
+                      "Ganesha node %(tag)s: %(db)s."),
332
+                      {'tag': self.tag, 'db': self.ganesha_db_path})
333
+            raise exception.InvalidSqliteDB()
334
+        return int(match.groups()[0])
335
+
336
+    def restart_service(self):
337
+        """Restart the Ganesha service."""
338
+        self.execute("service", self.ganesha_service, "restart")
339
+
340
+    def reset_exports(self):
341
+        """Delete all export files."""
342
+        self.execute('sh', '-c',
343
+                     'rm %s/*.conf' % pipes.quote(self.ganesha_export_dir))
344
+        self._mkindex()

+ 76
- 0
manila/share/drivers/ganesha/utils.py View File

@@ -0,0 +1,76 @@
1
+# Copyright (c) 2014 Red Hat, Inc.
2
+# All Rights Reserved.
3
+#
4
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+#    not use this file except in compliance with the License. You may obtain
6
+#    a copy of the License at
7
+#
8
+#         http://www.apache.org/licenses/LICENSE-2.0
9
+#
10
+#    Unless required by applicable law or agreed to in writing, software
11
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+#    License for the specific language governing permissions and limitations
14
+#    under the License.
15
+
16
+import os
17
+import pipes
18
+
19
+from oslo_concurrency import processutils
20
+import six
21
+
22
+from manila import utils
23
+
24
+
25
+def patch(base, *overlays):
26
+    """Recursive dictionary patching."""
27
+    for ovl in overlays:
28
+        for k, v in six.iteritems(ovl):
29
+            if isinstance(v, dict) and isinstance(base.get(k), dict):
30
+                patch(base[k], v)
31
+            else:
32
+                base[k] = v
33
+    return base
34
+
35
+
36
+def walk(dct):
37
+    """Recursive iteration over dictionary."""
38
+    for k, v in six.iteritems(dct):
39
+        if isinstance(v, dict):
40
+            for w in walk(v):
41
+                yield w
42
+        else:
43
+            yield k, v
44
+
45
+
46
+class RootExecutor(object):
47
+    """Execute wrapper defaulting to root exection."""
48
+
49
+    def __init__(self, execute=utils.execute):
50
+        self.execute = execute
51
+
52
+    def __call__(self, *args, **kwargs):
53
+        exkwargs = {"run_as_root": True}
54
+        exkwargs.update(kwargs)
55
+        return self.execute(*args, **exkwargs)
56
+
57
+
58
+class SSHExecutor(object):
59
+    """Callable encapsulating exec through ssh."""
60
+
61
+    def __init__(self, *args, **kwargs):
62
+        self.pool = utils.SSHPool(*args, **kwargs)
63
+
64
+    def __call__(self, *args, **kwargs):
65
+        cmd = ' '.join(pipes.quote(a) for a in args)
66
+        ssh = self.pool.get()
67
+        try:
68
+            ret = processutils.ssh_execute(ssh, cmd, **kwargs)
69
+        finally:
70
+            self.pool.put(ssh)
71
+        return ret
72
+
73
+
74
+def path_from(fpath, *rpath):
75
+    """Return the join of the dir of fpath and rpath in absolute form."""
76
+    return os.path.join(os.path.abspath(os.path.dirname(fpath)), *rpath)

+ 6
- 18
manila/share/drivers/ibm/gpfs.py View File

@@ -101,22 +101,6 @@ gpfs_share_opts = [
101 101
                      'NFS server. Note that these defaults can be overridden '
102 102
                      'when a share is created by passing metadata with key '
103 103
                      'name export_options.')),
104
-    cfg.StrOpt('gnfs_export_options',
105
-               default=('maxread = 65536, prefread = 65536'),
106
-               help=('Options to use when exporting a share using ganesha '
107
-                     'NFS server. Note that these defaults can be overridden '
108
-                     'when a share is created by passing metadata with key '
109
-                     'name export_options.  Also note the complete set of '
110
-                     'default ganesha export options is specified in '
111
-                     'ganesha_utils.')),
112
-    cfg.StrOpt('ganesha_config_path',
113
-               default='/etc/ganesha/ganesha_exports.conf',
114
-               help=('Path to ganesha export config file.  The config file '
115
-                     'may also contain non-export configuration data but it '
116
-                     'must be placed before the EXPORT clauses.')),
117
-    cfg.StrOpt('ganesha_service_name',
118
-               default='ganesha.nfsd',
119
-               help=('Name of the ganesha nfs service.')),
120 104
 ]
121 105
 
122 106
 
@@ -124,7 +108,9 @@ CONF = cfg.CONF
124 108
 CONF.register_opts(gpfs_share_opts)
125 109
 
126 110
 
127
-class GPFSShareDriver(driver.ExecuteMixin, driver.ShareDriver):
111
+class GPFSShareDriver(driver.ExecuteMixin, driver.GaneshaMixin,
112
+                      driver.ShareDriver):
113
+
128 114
     """GPFS Share Driver.
129 115
 
130 116
     Executes commands relating to Shares.
@@ -696,7 +682,9 @@ class GNFSHelper(NASHelperBase):
696 682
     def __init__(self, execute, config_object):
697 683
         super(GNFSHelper, self).__init__(execute, config_object)
698 684
         self.default_export_options = dict()
699
-        for m in AVPATTERN.finditer(self.configuration.gnfs_export_options):
685
+        for m in AVPATTERN.finditer(
686
+            self.configuration.ganesha_nfs_export_options
687
+        ):
700 688
             self.default_export_options[m.group('attr')] = m.group('val')
701 689
 
702 690
     def _get_export_options(self, share):

+ 0
- 0
manila/tests/share/drivers/ganesha/__init__.py View File


+ 518
- 0
manila/tests/share/drivers/ganesha/test_manager.py View File

@@ -0,0 +1,518 @@
1
+# Copyright (c) 2014 Red Hat, Inc.
2
+# All Rights Reserved.
3
+#
4
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+#    not use this file except in compliance with the License. You may obtain
6
+#    a copy of the License at
7
+#
8
+#         http://www.apache.org/licenses/LICENSE-2.0
9
+#
10
+#    Unless required by applicable law or agreed to in writing, software
11
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+#    License for the specific language governing permissions and limitations
14
+#    under the License.
15
+
16
+import contextlib
17
+import re
18
+
19
+import mock
20
+from oslo.serialization import jsonutils
21
+import six
22
+
23
+from manila import exception
24
+from manila.share.drivers.ganesha import manager
25
+from manila import test
26
+from manila import utils
27
+
28
+
29
+test_export_id = 101
30
+test_name = 'fakefile'
31
+test_path = '/fakedir0/export.d/fakefile.conf'
32
+test_ganesha_cnf = """EXPORT {
33
+    Export_Id = 101;
34
+    CLIENT {
35
+        Clients = ip1;
36
+    }
37
+}"""
38
+test_dict_unicode = {
39
+    u'EXPORT': {
40
+        u'Export_Id': 101,
41
+        u'CLIENT': {u'Clients': u"ip1"}
42
+    }
43
+}
44
+test_dict_str = {
45
+    'EXPORT': {
46
+        'Export_Id': 101,
47
+        'CLIENT': {'Clients': "ip1"}
48
+    }
49
+}
50
+
51
+manager_fake_kwargs = {
52
+    'ganesha_config_path': '/fakedir0/fakeconfig',
53
+    'ganesha_db_path': '/fakedir1/fake.db',
54
+    'ganesha_export_dir': '/fakedir0/export.d',
55
+    'ganesha_service_name': 'ganesha.fakeservice'
56
+}
57
+
58
+
59
+class GaneshaConfigTests(test.TestCase):
60
+    """Tests Ganesha config file format convertor functions."""
61
+
62
+    ref_ganesha_cnf = """EXPORT {
63
+    CLIENT {
64
+        Clients = ip1;
65
+    }
66
+    Export_Id = 101;
67
+}"""
68
+
69
+    @staticmethod
70
+    def conf_mangle(*confs):
71
+        """A "mangler" for the conf format.
72
+
73
+        Its purpose is to transform conf data in a way so that semantically
74
+        equivalent confs yield identical results. Besides this objective
75
+        criteria, we seek a good trade-off between the following
76
+        requirements:
77
+        - low lossiness;
78
+        - low code complexity.
79
+        """
80
+        def _conf_mangle(conf):
81
+            # split to expressions by the delimiter ";"
82
+            # (braces are forced to be treated as expressions
83
+            # by sandwiching them in ";"-s)
84
+            conf = re.sub('[{}]', ';\g<0>;', conf).split(';')
85
+            # whitespace-split expressions to tokens with
86
+            # (equality is forced to be treated as token by
87
+            # sandwiching in space)
88
+            conf = map(lambda l: l.replace("=", " = ").split(), conf)
89
+            # get rid of by-product empty lists (derived from superflouous
90
+            # ";"-s that might have crept in due to "sandwiching")
91
+            conf = map(lambda x: x, conf)
92
+            # handle the non-deterministic order of confs
93
+            conf.sort()
94
+            return conf
95
+
96
+        return (_conf_mangle(conf) for conf in confs)
97
+
98
+    def test_conf2json(self):
99
+        test_ganesha_cnf_with_comment = """EXPORT {
100
+# fake_export_block
101
+    Export_Id = 101;
102
+    CLIENT {
103
+        Clients = ip1;
104
+    }
105
+}"""
106
+        ret = manager._conf2json(test_ganesha_cnf_with_comment)
107
+        self.assertEqual(test_dict_unicode, jsonutils.loads(ret))
108
+
109
+    def test_parseconf_ganesha_cnf_input(self):
110
+        ret = manager.parseconf(test_ganesha_cnf)
111
+        self.assertEqual(test_dict_unicode, ret)
112
+
113
+    def test_parseconf_json_input(self):
114
+        ret = manager.parseconf(jsonutils.dumps(test_dict_str))
115
+        self.assertEqual(test_dict_unicode, ret)
116
+
117
+    def test_dump_to_conf(self):
118
+        ganesha_cnf = six.StringIO()
119
+        manager._dump_to_conf(test_dict_str, ganesha_cnf)
120
+        self.assertEqual(*self.conf_mangle(self.ref_ganesha_cnf,
121
+                                           ganesha_cnf.getvalue()))
122
+
123
+    def test_mkconf(self):
124
+        ganesha_cnf = manager.mkconf(test_dict_str)
125
+        self.assertEqual(*self.conf_mangle(self.ref_ganesha_cnf,
126
+                                           ganesha_cnf))
127
+
128
+
129
+class GaneshaManagerTestCase(test.TestCase):
130
+    """Tests GaneshaManager."""
131
+
132
+    def setUp(self):
133
+        super(GaneshaManagerTestCase, self).setUp()
134
+        self._execute = mock.Mock(return_value=('', ''))
135
+        with contextlib.nested(
136
+            mock.patch.object(manager.GaneshaManager, 'get_export_id',
137
+                              return_value=100),
138
+            mock.patch.object(manager.GaneshaManager, 'reset_exports'),
139
+            mock.patch.object(manager.GaneshaManager, 'restart_service')
140
+        ) as (self.mock_get_export_id, self.mock_reset_exports,
141
+              self.mock_restart_service):
142
+            self._manager = manager.GaneshaManager(
143
+                self._execute, 'faketag', **manager_fake_kwargs)
144
+        self.stubs.Set(utils, 'synchronized',
145
+                       mock.Mock(return_value=lambda f: f))
146
+
147
+    def test_init(self):
148
+        self.stubs.Set(self._manager, 'reset_exports', mock.Mock())
149
+        self.stubs.Set(self._manager, 'restart_service', mock.Mock())
150
+        self.assertEqual('/fakedir0/fakeconfig',
151
+                         self._manager.ganesha_config_path)
152
+        self.assertEqual('faketag', self._manager.tag)
153
+        self.assertEqual('/fakedir0/export.d',
154
+                         self._manager.ganesha_export_dir)
155
+        self.assertEqual('/fakedir1/fake.db', self._manager.ganesha_db_path)
156
+        self.assertEqual('ganesha.fakeservice', self._manager.ganesha_service)
157
+        self.assertEqual(
158
+            [mock.call('mkdir', '-p', self._manager.ganesha_export_dir),
159
+             mock.call('mkdir', '-p', '/fakedir1'),
160
+             mock.call('sqlite3', self._manager.ganesha_db_path,
161
+                       'create table ganesha(key varchar(20) primary key, '
162
+                       'value int); insert into ganesha values("exportid", '
163
+                       '100);', run_as_root=False, check_exit_code=False)],
164
+            self._execute.call_args_list)
165
+        self.mock_get_export_id.assert_called_once_with(bump=False)
166
+        self.mock_reset_exports.assert_called_once_with()
167
+        self.mock_restart_service.assert_called_once_with()
168
+
169
+    def test_init_execute_error_log_message(self):
170
+        fake_args = ('foo', 'bar')
171
+
172
+        def raise_exception(*args, **kwargs):
173
+            if args == fake_args:
174
+                raise exception.GaneshaCommandFailure()
175
+
176
+        test_execute = mock.Mock(side_effect=raise_exception)
177
+        self.stubs.Set(manager.LOG, 'error', mock.Mock())
178
+        with contextlib.nested(
179
+            mock.patch.object(manager.GaneshaManager, 'get_export_id',
180
+                              return_value=100),
181
+            mock.patch.object(manager.GaneshaManager, 'reset_exports'),
182
+            mock.patch.object(manager.GaneshaManager, 'restart_service')
183
+        ) as (self.mock_get_export_id, self.mock_reset_exports,
184
+              self.mock_restart_service):
185
+            test_manager = manager.GaneshaManager(
186
+                test_execute, 'faketag', **manager_fake_kwargs)
187
+        self.assertRaises(
188
+            exception.GaneshaCommandFailure,
189
+            test_manager.execute,
190
+            *fake_args, message='fakemsg')
191
+        manager.LOG.error.assert_called_once_with(
192
+            mock.ANY, {'tag': 'faketag', 'msg': 'fakemsg'})
193
+
194
+    def test_init_execute_error_no_log_message(self):
195
+        fake_args = ('foo', 'bar')
196
+
197
+        def raise_exception(*args, **kwargs):
198
+            if args == fake_args:
199
+                raise exception.GaneshaCommandFailure()
200
+
201
+        test_execute = mock.Mock(side_effect=raise_exception)
202
+        self.stubs.Set(manager.LOG, 'error', mock.Mock())
203
+        with contextlib.nested(
204
+            mock.patch.object(manager.GaneshaManager, 'get_export_id',
205
+                              return_value=100),
206
+            mock.patch.object(manager.GaneshaManager, 'reset_exports'),
207
+            mock.patch.object(manager.GaneshaManager, 'restart_service')
208
+        ) as (self.mock_get_export_id, self.mock_reset_exports,
209
+              self.mock_restart_service):
210
+            test_manager = manager.GaneshaManager(
211
+                test_execute, 'faketag', **manager_fake_kwargs)
212
+        self.assertRaises(
213
+            exception.GaneshaCommandFailure,
214
+            test_manager.execute,
215
+            *fake_args, message='fakemsg', makelog=False)
216
+        self.assertFalse(manager.LOG.error.called)
217
+
218
+    def test_ganesha_export_dir(self):
219
+        self.assertEqual(
220
+            '/fakedir0/export.d', self._manager.ganesha_export_dir)
221
+
222
+    def test_getpath(self):
223
+        self.assertEqual(
224
+            '/fakedir0/export.d/fakefile.conf',
225
+            self._manager._getpath('fakefile'))
226
+
227
+    def test_write_file(self):
228
+        test_data = 'fakedata'
229
+        self.stubs.Set(manager.pipes, 'quote',
230
+                       mock.Mock(return_value='fakefile.conf.RANDOM'))
231
+        test_args = [
232
+            ('mktemp', '-p', '/fakedir0/export.d', '-t',
233
+             'fakefile.conf.XXXXXX'),
234
+            ('sh', '-c', 'cat > fakefile.conf.RANDOM'),
235
+            ('mv', 'fakefile.conf.RANDOM', test_path)]
236
+        test_kwargs = {
237
+            'process_input': test_data,
238
+            'message': 'writing fakefile.conf.RANDOM'
239
+        }
240
+
241
+        def return_tmpfile(*args, **kwargs):
242
+            if args == test_args[0]:
243
+                return ('fakefile.conf.RANDOM\n', '')
244
+
245
+        self.stubs.Set(self._manager, 'execute',
246
+                       mock.Mock(side_effect=return_tmpfile))
247
+        self._manager._write_file(test_path, test_data)
248
+        self._manager.execute.assert_has_calls([
249
+            mock.call(*test_args[0]),
250
+            mock.call(*test_args[1], **test_kwargs),
251
+            mock.call(*test_args[2])])
252
+        manager.pipes.quote.assert_called_once_with('fakefile.conf.RANDOM')
253
+
254
+    def test_write_conf_file(self):
255
+        test_data = 'fakedata'
256
+        self.stubs.Set(self._manager, '_getpath',
257
+                       mock.Mock(return_value=test_path))
258
+        self.stubs.Set(self._manager, '_write_file', mock.Mock())
259
+        ret = self._manager._write_conf_file(test_name, test_data)
260
+        self.assertEqual(test_path, ret)
261
+        self._manager._getpath.assert_called_once_with(test_name)
262
+        self._manager._write_file.assert_called_once_with(
263
+            test_path, test_data)
264
+
265
+    def test_mkindex(self):
266
+        test_ls_output = 'INDEX.conf\nfakefile.conf\nfakefile.txt'
267
+        test_index = '%include /fakedir0/export.d/fakefile.conf\n'
268
+        self.stubs.Set(self._manager, 'execute',
269
+                       mock.Mock(return_value=(test_ls_output, '')))
270
+        self.stubs.Set(self._manager, '_write_conf_file', mock.Mock())
271
+        ret = self._manager._mkindex()
272
+        self._manager.execute.assert_called_once_with(
273
+            'ls', '/fakedir0/export.d', run_as_root=False)
274
+        self._manager._write_conf_file.assert_called_once_with(
275
+            'INDEX', test_index)
276
+        self.assertEqual(None, ret)
277
+
278
+    def test_read_export_file(self):
279
+        test_args = ('cat', test_path)
280
+        test_kwargs = {'message': 'reading export fakefile'}
281
+        self.stubs.Set(self._manager, '_getpath',
282
+                       mock.Mock(return_value=test_path))
283
+        self.stubs.Set(self._manager, 'execute',
284
+                       mock.Mock(return_value=(test_ganesha_cnf,)))
285
+        self.stubs.Set(manager, 'parseconf',
286
+                       mock.Mock(return_value=test_dict_unicode))
287
+        ret = self._manager._read_export_file(test_name)
288
+        self._manager._getpath.assert_called_once_with(test_name)
289
+        self._manager.execute.assert_called_once_with(
290
+            *test_args, **test_kwargs)
291
+        manager.parseconf.assert_called_once_with(test_ganesha_cnf)
292
+        self.assertEqual(test_dict_unicode, ret)
293
+
294
+    def test_write_export_file(self):
295
+        self.stubs.Set(manager, 'mkconf',
296
+                       mock.Mock(return_value=test_ganesha_cnf))
297
+        self.stubs.Set(self._manager, '_write_conf_file',
298
+                       mock.Mock(return_value=test_path))
299
+        ret = self._manager._write_export_file(test_name, test_dict_str)
300
+        manager.mkconf.assert_called_once_with(test_dict_str)
301
+        self._manager._write_conf_file.assert_called_once_with(
302
+            test_name, test_ganesha_cnf)
303
+        self.assertEqual(test_path, ret)
304
+
305
+    def test_write_export_file_error_incomplete_export_block(self):
306
+
307
+        test_errordict = {
308
+            u'EXPORT': {
309
+                u'Export_Id': '@config',
310
+                u'CLIENT': {u'Clients': u"'ip1','ip2'"}
311
+            }
312
+        }
313
+        self.stubs.Set(manager, 'mkconf',
314
+                       mock.Mock(return_value=test_ganesha_cnf))
315
+        self.stubs.Set(self._manager, '_write_conf_file',
316
+                       mock.Mock(return_value=test_path))
317
+        self.assertRaises(exception.InvalidParameterValue,
318
+                          self._manager._write_export_file,
319
+                          test_name, test_errordict)
320
+        self.assertFalse(manager.mkconf.called)
321
+        self.assertFalse(self._manager._write_conf_file.called)
322
+
323
+    def test_rm_export_file(self):
324
+        self.stubs.Set(self._manager, 'execute',
325
+                       mock.Mock(return_value=('', '')))
326
+        self.stubs.Set(self._manager, '_getpath',
327
+                       mock.Mock(return_value=test_path))
328
+        ret = self._manager._rm_export_file(test_name)
329
+        self._manager._getpath.assert_called_once_with(test_name)
330
+        self._manager.execute.assert_called_once_with('rm', test_path)
331
+        self.assertEqual(None, ret)
332
+
333
+    def test_dbus_send_ganesha(self):
334
+        test_args = ('arg1', 'arg2')
335
+        test_kwargs = {'key': 'value'}
336
+        self.stubs.Set(self._manager, 'execute',
337
+                       mock.Mock(return_value=('', '')))
338
+        ret = self._manager._dbus_send_ganesha('fakemethod', *test_args,
339
+                                               **test_kwargs)
340
+        self._manager.execute.assert_called_once_with(
341
+            'dbus-send', '--print-reply', '--system',
342
+            '--dest=org.ganesha.nfsd', '/org/ganesha/nfsd/ExportMgr',
343
+            'org.ganesha.nfsd.exportmgr.fakemethod',
344
+            *test_args, message='dbus call exportmgr.fakemethod',
345
+            **test_kwargs)
346
+        self.assertEqual(None, ret)
347
+
348
+    def test_remove_export_dbus(self):
349
+        self.stubs.Set(self._manager, '_dbus_send_ganesha',
350
+                       mock.Mock())
351
+        ret = self._manager._remove_export_dbus(test_export_id)
352
+        self._manager._dbus_send_ganesha.assert_called_once_with(
353
+            'RemoveExport', 'uint16:101')
354
+        self.assertEqual(None, ret)
355
+
356
+    def test_add_export(self):
357
+        self.stubs.Set(self._manager, '_write_export_file',
358
+                       mock.Mock(return_value=test_path))
359
+        self.stubs.Set(self._manager, '_dbus_send_ganesha', mock.Mock())
360
+        self.stubs.Set(self._manager, '_mkindex', mock.Mock())
361
+        ret = self._manager.add_export(test_name, test_dict_str)
362
+        self._manager._write_export_file.assert_called_once_with(
363
+            test_name, test_dict_str)
364
+        self._manager._dbus_send_ganesha.assert_called_once_with(
365
+            'AddExport', 'string:' + test_path,
366
+            'string:EXPORT(Export_Id=101)')
367
+        self._manager._mkindex.assert_called_once_with()
368
+        self.assertEqual(None, ret)
369
+
370
+    def test_add_export_error_during_mkindex(self):
371
+        self.stubs.Set(self._manager, '_write_export_file',
372
+                       mock.Mock(return_value=test_path))
373
+        self.stubs.Set(self._manager, '_dbus_send_ganesha', mock.Mock())
374
+        self.stubs.Set(self._manager, '_mkindex',
375
+                       mock.Mock(side_effect=exception.GaneshaCommandFailure))
376
+        self.stubs.Set(self._manager, '_rm_export_file', mock.Mock())
377
+        self.stubs.Set(self._manager, '_remove_export_dbus', mock.Mock())
378
+        self.assertRaises(exception.GaneshaCommandFailure,
379
+                          self._manager.add_export, test_name, test_dict_str)
380
+        self._manager._write_export_file.assert_called_once_with(
381
+            test_name, test_dict_str)
382
+        self._manager._dbus_send_ganesha.assert_called_once_with(
383
+            'AddExport', 'string:' + test_path,
384
+            'string:EXPORT(Export_Id=101)')
385
+        self._manager._mkindex.assert_called_once_with()
386
+        self._manager._rm_export_file.assert_called_once_with(test_name)
387
+        self._manager._remove_export_dbus.assert_called_once_with(
388
+            test_export_id)
389
+
390
+    def test_add_export_error_during_write_export_file(self):
391
+        self.stubs.Set(self._manager, '_write_export_file',
392
+                       mock.Mock(side_effect=exception.GaneshaCommandFailure))
393
+        self.stubs.Set(self._manager, '_dbus_send_ganesha', mock.Mock())
394
+        self.stubs.Set(self._manager, '_mkindex', mock.Mock())
395
+        self.stubs.Set(self._manager, '_rm_export_file', mock.Mock())
396
+        self.stubs.Set(self._manager, '_remove_export_dbus', mock.Mock())
397
+        self.assertRaises(exception.GaneshaCommandFailure,
398
+                          self._manager.add_export, test_name, test_dict_str)
399
+        self._manager._write_export_file.assert_called_once_with(
400
+            test_name, test_dict_str)
401
+        self.assertFalse(self._manager._dbus_send_ganesha.called)
402
+        self._manager._mkindex.assert_called_once_with()
403
+        self.assertFalse(self._manager._rm_export_file.called)
404
+        self.assertFalse(self._manager._remove_export_dbus.called)
405
+
406
+    def test_add_export_error_during_dbus_send_ganesha(self):
407
+        self.stubs.Set(self._manager, '_write_export_file',
408
+                       mock.Mock(return_value=test_path))
409
+        self.stubs.Set(self._manager, '_dbus_send_ganesha',
410
+                       mock.Mock(side_effect=exception.GaneshaCommandFailure))
411
+        self.stubs.Set(self._manager, '_mkindex',
412
+                       mock.Mock())
413
+        self.stubs.Set(self._manager, '_rm_export_file', mock.Mock())
414
+        self.stubs.Set(self._manager, '_remove_export_dbus', mock.Mock())
415
+        self.assertRaises(exception.GaneshaCommandFailure,
416
+                          self._manager.add_export, test_name, test_dict_str)
417
+        self._manager._write_export_file.assert_called_once_with(
418
+            test_name, test_dict_str)
419
+        self._manager._dbus_send_ganesha.assert_called_once_with(
420
+            'AddExport', 'string:' + test_path,
421
+            'string:EXPORT(Export_Id=101)')
422
+        self._manager._rm_export_file.assert_called_once_with(test_name)
423
+        self._manager._mkindex.assert_called_once_with()
424
+        self.assertFalse(self._manager._remove_export_dbus.called)
425
+
426
+    def test_remove_export(self):
427
+        self.stubs.Set(self._manager, '_read_export_file',
428
+                       mock.Mock(return_value=test_dict_unicode))
429
+        methods = ('_remove_export_dbus', '_rm_export_file', '_mkindex')
430
+        for method in methods:
431
+            self.stubs.Set(self._manager, method, mock.Mock())
432
+        ret = self._manager.remove_export(test_name)
433
+        self._manager._read_export_file.assert_called_once_with(test_name)
434
+        self._manager._remove_export_dbus.assert_called_once_with(
435
+            test_dict_unicode['EXPORT']['Export_Id'])
436
+        self._manager._rm_export_file.assert_called_once_with(test_name)
437
+        self._manager._mkindex.assert_called_once_with()
438
+        self.assertEqual(None, ret)
439
+
440
+    def test_remove_export_error_during_read_export_file(self):
441
+        self.stubs.Set(self._manager, '_read_export_file',
442
+                       mock.Mock(side_effect=exception.GaneshaCommandFailure))
443
+        methods = ('_remove_export_dbus', '_rm_export_file', '_mkindex')
444
+        for method in methods:
445
+            self.stubs.Set(self._manager, method, mock.Mock())
446
+        self.assertRaises(exception.GaneshaCommandFailure,
447
+                          self._manager.remove_export, test_name)
448
+        self._manager._read_export_file.assert_called_once_with(test_name)
449
+        self.assertFalse(self._manager._remove_export_dbus.called)
450
+        self._manager._rm_export_file.assert_called_once_with(test_name)
451
+        self._manager._mkindex.assert_called_once_with()
452
+
453
+    def test_remove_export_error_during_remove_export_dbus(self):
454
+        self.stubs.Set(self._manager, '_read_export_file',
455
+                       mock.Mock(return_value=test_dict_unicode))
456
+        self.stubs.Set(self._manager, '_remove_export_dbus',
457
+                       mock.Mock(side_effect=exception.GaneshaCommandFailure))
458
+        methods = ('_rm_export_file', '_mkindex')
459
+        for method in methods:
460
+            self.stubs.Set(self._manager, method, mock.Mock())
461
+        self.assertRaises(exception.GaneshaCommandFailure,
462
+                          self._manager.remove_export, test_name)
463
+        self._manager._read_export_file.assert_called_once_with(test_name)
464
+        self._manager._remove_export_dbus.assert_called_once_with(
465
+            test_dict_unicode['EXPORT']['Export_Id'])
466
+        self._manager._rm_export_file.assert_called_once_with(test_name)
467
+        self._manager._mkindex.assert_called_once_with()
468
+
469
+    def test_get_export_id(self):
470
+        self.stubs.Set(self._manager, 'execute',
471
+                       mock.Mock(return_value=('exportid|101', '')))
472
+        ret = self._manager.get_export_id()
473
+        self._manager.execute.assert_called_once_with(
474
+            'sqlite3', self._manager.ganesha_db_path,
475
+            'update ganesha set value = value + 1;'
476
+            'select * from ganesha where key = "exportid";',
477
+            run_as_root=False)
478
+        self.assertEqual(101, ret)
479
+
480
+    def test_get_export_id_nobump(self):
481
+        self.stubs.Set(self._manager, 'execute',
482
+                       mock.Mock(return_value=('exportid|101', '')))
483
+        ret = self._manager.get_export_id(bump=False)
484
+        self._manager.execute.assert_called_once_with(
485
+            'sqlite3', self._manager.ganesha_db_path,
486
+            'select * from ganesha where key = "exportid";',
487
+            run_as_root=False)
488
+        self.assertEqual(101, ret)
489
+
490
+    def test_get_export_id_error_invalid_export_db(self):
491
+        self.stubs.Set(self._manager, 'execute',
492
+                       mock.Mock(return_value=('invalid', '')))
493
+        self.stubs.Set(manager.LOG, 'error', mock.Mock())
494
+        self.assertRaises(exception.InvalidSqliteDB,
495
+                          self._manager.get_export_id)
496
+        manager.LOG.error.assert_called_once_with(
497
+            mock.ANY, mock.ANY)
498
+        self._manager.execute.assert_called_once_with(
499
+            'sqlite3', self._manager.ganesha_db_path,
500
+            'update ganesha set value = value + 1;'
501
+            'select * from ganesha where key = "exportid";',
502
+            run_as_root=False)
503
+
504
+    def test_restart_service(self):
505
+        self.stubs.Set(self._manager, 'execute', mock.Mock())
506
+        ret = self._manager.restart_service()
507
+        self._manager.execute.assert_called_once_with(
508
+            'service', 'ganesha.fakeservice', 'restart')
509
+        self.assertEqual(None, ret)
510
+
511
+    def test_reset_exports(self):
512
+        self.stubs.Set(self._manager, 'execute', mock.Mock())
513
+        self.stubs.Set(self._manager, '_mkindex', mock.Mock())
514
+        ret = self._manager.reset_exports()
515
+        self._manager.execute.assert_called_once_with(
516
+            'sh', '-c', 'rm /fakedir0/export.d/*.conf')
517
+        self._manager._mkindex.assert_called_once_with()
518
+        self.assertEqual(None, ret)

+ 51
- 0
manila/tests/share/drivers/ganesha/test_utils.py View File

@@ -0,0 +1,51 @@
1
+# Copyright (c) 2014 Red Hat, Inc.
2
+# All Rights Reserved.
3
+#
4
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+#    not use this file except in compliance with the License. You may obtain
6
+#    a copy of the License at
7
+#
8
+#         http://www.apache.org/licenses/LICENSE-2.0
9
+#
10
+#    Unless required by applicable law or agreed to in writing, software
11
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+#    License for the specific language governing permissions and limitations
14
+#    under the License.
15
+
16
+import os
17
+
18
+from manila.share.drivers.ganesha import utils
19
+from manila import test
20
+
21
+
22
+patch_test_dict1 = {'a': 1, 'b': {'c': 2}, 'd': 3, 'e': 4}
23
+patch_test_dict2 = {'a': 11, 'b': {'f': 5}, 'd': {'g': 6}}
24
+patch_test_dict3 = {'b': {'c': 22, 'h': {'i': 7}}, 'e': None}
25
+patch_test_dict_result = {
26
+    'a': 11,
27
+    'b': {'c': 22, 'f': 5, 'h': {'i': 7}},
28
+    'd': {'g': 6},
29
+    'e': None,
30
+}
31
+
32
+walk_test_dict = {'a': {'b': {'c': {'d': {'e': 'f'}}}}}
33
+walk_test_list = [('e', 'f')]
34
+
35
+
36
+class GaneshaUtilsTests(test.TestCase):
37
+    """Tests Ganesha utility functions."""
38
+
39
+    def test_patch(self):
40
+        ret = utils.patch(patch_test_dict1, patch_test_dict2, patch_test_dict3)
41
+        self.assertEqual(patch_test_dict_result, ret)
42
+
43
+    def test_walk(self):
44
+        ret = [elem for elem in utils.walk(walk_test_dict)]
45
+        self.assertEqual(walk_test_list, ret)
46
+
47
+    def test_path_from(self):
48
+        self.stubs.Set(os.path, 'abspath',
49
+                       lambda path: os.path.join('/foo/bar', path))
50
+        ret = utils.path_from('baz.py', '../quux', 'tic/tac/toe')
51
+        self.assertEqual('/foo/quux/tic/tac/toe', os.path.normpath(ret))

+ 276
- 0
manila/tests/share/drivers/test_ganesha.py View File

@@ -0,0 +1,276 @@
1
+# Copyright (c) 2014 Red Hat, Inc.
2
+# All Rights Reserved.
3
+#
4
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+#    not use this file except in compliance with the License. You may obtain
6
+#    a copy of the License at
7
+#
8
+#         http://www.apache.org/licenses/LICENSE-2.0
9
+#
10
+#    Unless required by applicable law or agreed to in writing, software
11
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+#    License for the specific language governing permissions and limitations
14
+#    under the License.
15
+
16
+import copy
17
+import errno
18
+import os
19
+
20
+import mock
21
+from oslo.config import cfg
22
+
23
+from manila import exception
24
+from manila.share import configuration as config
25
+from manila.share.drivers import ganesha
26
+from manila import test
27
+from manila.tests.db import fakes as db_fakes
28
+
29
+CONF = cfg.CONF
30
+
31
+
32
+def fake_access(**kwargs):
33
+    access = {
34
+        'id': 'fakeaccessid',
35
+        'access_type': 'ip',
36
+        'access_to': '10.0.0.1'
37
+    }
38
+    access.update(kwargs)
39
+    return db_fakes.FakeModel(access)
40
+
41
+
42
+def fake_share(**kwargs):
43
+    share = {
44
+        'id': 'fakeid',
45
+        'name': 'fakename',
46
+        'size': 1,
47
+        'share_proto': 'NFS',
48
+        'export_location': '127.0.0.1:/mnt/nfs/testvol',
49
+    }
50
+    share.update(kwargs)
51
+    return db_fakes.FakeModel(share)
52
+
53
+fake_basepath = '/fakepath'
54
+
55
+fake_export_name = 'fakename--fakeaccessid'
56
+
57
+fake_output_template = {
58
+    'EXPORT': {
59
+        'Export_Id': 101,
60
+        'Path': '/fakepath/fakename',
61
+        'Pseudo': '/fakepath/fakename--fakeaccessid',
62
+        'Tag': 'fakeaccessid',
63
+        'CLIENT': {
64
+            'Clients': '10.0.0.1'
65
+        },
66
+        'FSAL': 'fakefsal'
67
+    }
68
+}
69
+
70
+
71
+class GaneshaNASHelperTestCase(test.TestCase):
72
+    """Tests GaneshaNASHElper."""
73
+
74
+    def setUp(self):
75
+        super(GaneshaNASHelperTestCase, self).setUp()
76
+
77
+        CONF.set_default('ganesha_config_path', '/fakedir0/fakeconfig')
78
+        CONF.set_default('ganesha_db_path', '/fakedir1/fake.db')
79
+        CONF.set_default('ganesha_export_dir', '/fakedir0/export.d')
80
+        CONF.set_default('ganesha_export_template_dir',
81
+                         '/fakedir2/faketempl.d')
82
+        CONF.set_default('ganesha_service_name', 'ganesha.fakeservice')
83
+        self._execute = mock.Mock(return_value=('', ''))
84
+        self.fake_conf = config.Configuration(None)
85
+        self.fake_conf_dir_path = '/fakedir0/exports.d'
86
+        self._helper = ganesha.GaneshaNASHelper(
87
+            self._execute, self.fake_conf, tag='faketag')
88
+        self._helper.ganesha = mock.Mock()
89
+        self._helper.export_template = {'key': 'value'}
90
+        self.share = fake_share()
91
+        self.access = fake_access()
92
+
93
+    def test_load_conf_dir(self):
94
+        fake_template1 = {'key': 'value1'}
95
+        fake_template2 = {'key': 'value2'}
96
+        fake_ls_dir = ['fakefile0.conf', 'fakefile1.json', 'fakefile2.txt']
97
+        mock_ganesha_utils_patch = mock.Mock()
98
+
99
+        def fake_patch_run(tmpl1, tmpl2):
100
+            mock_ganesha_utils_patch(
101
+                copy.deepcopy(tmpl1), copy.deepcopy(tmpl2))
102
+            tmpl1.update(tmpl2)
103
+
104
+        self.stubs.Set(ganesha.os, 'listdir',
105
+                       mock.Mock(return_value=fake_ls_dir))
106
+        self.stubs.Set(ganesha.LOG, 'info', mock.Mock())
107
+        self.stubs.Set(ganesha.ganesha_manager, 'parseconf',
108
+                       mock.Mock(side_effect=[fake_template1,
109
+                                              fake_template2]))
110
+        self.stubs.Set(ganesha.ganesha_utils, 'patch',
111
+                       mock.Mock(side_effect=fake_patch_run))
112
+        with mock.patch('six.moves.builtins.open',
113
+                        mock.mock_open()) as mockopen:
114
+            mockopen().read.side_effect = ['fakeconf0', 'fakeconf1']
115
+            ret = self._helper._load_conf_dir(self.fake_conf_dir_path)
116
+            ganesha.os.listdir.assert_called_once_with(
117
+                self.fake_conf_dir_path)
118
+            ganesha.LOG.info.assert_called_once_with(
119
+                mock.ANY, self.fake_conf_dir_path)
120
+            mockopen.assert_has_calls([
121
+                mock.call('/fakedir0/exports.d/fakefile0.conf'),
122
+                mock.call('/fakedir0/exports.d/fakefile1.json')],
123
+                any_order=True)
124
+            ganesha.ganesha_manager.parseconf.assert_has_calls([
125
+                mock.call('fakeconf0'), mock.call('fakeconf1')])
126
+            mock_ganesha_utils_patch.assert_has_calls([
127
+                mock.call({}, fake_template1),
128
+                mock.call(fake_template1, fake_template2)])
129
+            self.assertEqual(fake_template2, ret)
130
+
131
+    def test_load_conf_dir_no_conf_dir_must_exist_false(self):
132
+        self.stubs.Set(
133
+            ganesha.os, 'listdir',
134
+            mock.Mock(side_effect=OSError(errno.ENOENT,
135
+                                          os.strerror(errno.ENOENT))))
136
+        self.stubs.Set(ganesha.LOG, 'info', mock.Mock())
137
+        self.stubs.Set(ganesha.ganesha_manager, 'parseconf', mock.Mock())
138
+        self.stubs.Set(ganesha.ganesha_utils, 'patch', mock.Mock())
139
+        with mock.patch('six.moves.builtins.open',
140
+                        mock.mock_open(read_data='fakeconf')) as mockopen:
141
+            ret = self._helper._load_conf_dir(self.fake_conf_dir_path,
142
+                                              must_exist=False)
143
+            ganesha.os.listdir.assert_called_once_with(
144
+                self.fake_conf_dir_path)
145
+            ganesha.LOG.info.assert_called_once_with(
146
+                mock.ANY, self.fake_conf_dir_path)
147
+            self.assertFalse(mockopen.called)
148
+            self.assertFalse(ganesha.ganesha_manager.parseconf.called)
149
+            self.assertFalse(ganesha.ganesha_utils.patch.called)
150
+            self.assertEqual({}, ret)
151
+
152
+    def test_load_conf_dir_error_no_conf_dir_must_exist_true(self):
153
+        self.stubs.Set(
154
+            ganesha.os, 'listdir',
155
+            mock.Mock(side_effect=OSError(errno.ENOENT,
156
+                                          os.strerror(errno.ENOENT))))
157
+        self.assertRaises(OSError, self._helper._load_conf_dir,
158
+                          self.fake_conf_dir_path)
159
+        ganesha.os.listdir.assert_called_once_with(self.fake_conf_dir_path)
160
+
161
+    def test_load_conf_dir_error_conf_dir_present_must_exist_false(self):
162
+        self.stubs.Set(
163
+            ganesha.os, 'listdir',
164
+            mock.Mock(side_effect=OSError(errno.EACCES,
165
+                                          os.strerror(errno.EACCES))))
166
+        self.assertRaises(OSError, self._helper._load_conf_dir,
167
+                          self.fake_conf_dir_path, must_exist=False)
168
+        ganesha.os.listdir.assert_called_once_with(self.fake_conf_dir_path)
169
+
170
+    def test_load_conf_dir_error(self):
171
+        self.stubs.Set(
172
+            ganesha.os, 'listdir',
173
+            mock.Mock(side_effect=RuntimeError('fake error')))
174
+        self.assertRaises(RuntimeError, self._helper._load_conf_dir,
175
+                          self.fake_conf_dir_path)
176
+        ganesha.os.listdir.assert_called_once_with(self.fake_conf_dir_path)
177
+
178
+    def test_init_helper(self):
179
+        mock_template = mock.Mock()
180
+        mock_ganesha_manager = mock.Mock()
181
+        self.stubs.Set(ganesha.ganesha_manager, 'GaneshaManager',
182
+                       mock.Mock(return_value=mock_ganesha_manager))
183
+        self.stubs.Set(self._helper, '_load_conf_dir',
184
+                       mock.Mock(return_value=mock_template))
185
+        self.stubs.Set(self._helper, '_default_config_hook', mock.Mock())
186
+        ret = self._helper.init_helper()
187
+        ganesha.ganesha_manager.GaneshaManager.assert_called_once_with(
188
+            self._execute, 'faketag',
189
+            ganesha_config_path='/fakedir0/fakeconfig',
190
+            ganesha_export_dir='/fakedir0/export.d',
191
+            ganesha_db_path='/fakedir1/fake.db',
192
+            ganesha_service_name='ganesha.fakeservice')
193
+        self._helper._load_conf_dir.assert_called_once_with(
194
+            '/fakedir2/faketempl.d', must_exist=False)
195
+        self.assertFalse(self._helper._default_config_hook.called)
196
+        self.assertEqual(mock_ganesha_manager, self._helper.ganesha)
197
+        self.assertEqual(mock_template, self._helper.export_template)
198
+        self.assertEqual(None, ret)
199
+
200
+    def test_init_helper_conf_dir_empty(self):
201
+        mock_template = mock.Mock()
202
+        mock_ganesha_manager = mock.Mock()
203
+        self.stubs.Set(ganesha.ganesha_manager, 'GaneshaManager',
204
+                       mock.Mock(return_value=mock_ganesha_manager))
205
+        self.stubs.Set(self._helper, '_load_conf_dir',
206
+                       mock.Mock(return_value={}))
207
+        self.stubs.Set(self._helper, '_default_config_hook',
208
+                       mock.Mock(return_value=mock_template))
209
+        ret = self._helper.init_helper()
210
+        ganesha.ganesha_manager.GaneshaManager.assert_called_once_with(
211
+            self._execute, 'faketag',
212
+            ganesha_config_path='/fakedir0/fakeconfig',
213
+            ganesha_export_dir='/fakedir0/export.d',
214
+            ganesha_db_path='/fakedir1/fake.db',
215
+            ganesha_service_name='ganesha.fakeservice')
216
+        self._helper._load_conf_dir.assert_called_once_with(
217
+            '/fakedir2/faketempl.d', must_exist=False)
218
+        self._helper._default_config_hook.assert_called_once_with()
219
+        self.assertEqual(mock_ganesha_manager, self._helper.ganesha)
220
+        self.assertEqual(mock_template, self._helper.export_template)
221
+        self.assertEqual(None, ret)
222
+
223
+    def test_default_config_hook(self):
224
+        fake_template = {'key': 'value'}
225
+        self.stubs.Set(ganesha.ganesha_utils, 'path_from',
226
+                       mock.Mock(return_value='/fakedir3/fakeconfdir'))
227
+        self.stubs.Set(self._helper, '_load_conf_dir',
228
+                       mock.Mock(return_value=fake_template))
229
+        ret = self._helper._default_config_hook()
230
+        ganesha.ganesha_utils.path_from.assert_called_once_with(
231
+            ganesha.__file__, 'conf')
232
+        self._helper._load_conf_dir.assert_called_once_with(
233
+            '/fakedir3/fakeconfdir')
234
+        self.assertEqual(fake_template, ret)
235
+
236
+    def test_fsal_hook(self):
237
+        ret = self._helper._fsal_hook('/fakepath', self.share, self.access)
238
+        self.assertEqual({}, ret)
239
+
240
+    def test_allow_access(self):
241
+        mock_ganesha_utils_patch = mock.Mock()
242
+
243
+        def fake_patch_run(tmpl1, tmpl2, tmpl3):
244
+            mock_ganesha_utils_patch(copy.deepcopy(tmpl1), tmpl2, tmpl3)
245
+            tmpl1.update(tmpl3)
246
+
247
+        self.stubs.Set(self._helper.ganesha, 'get_export_id',
248
+                       mock.Mock(return_value=101))
249
+        self.stubs.Set(self._helper, '_fsal_hook',
250
+                       mock.Mock(return_value='fakefsal'))
251
+        self.stubs.Set(ganesha.ganesha_utils, 'patch',
252
+                       mock.Mock(side_effect=fake_patch_run))
253
+        ret = self._helper.allow_access(fake_basepath, self.share,
254
+                                        self.access)
255
+        self._helper.ganesha.get_export_id.assert_called_once_with()
256
+        self._helper._fsal_hook.assert_called_once_with(
257
+            fake_basepath, self.share, self.access)
258
+        mock_ganesha_utils_patch.assert_called_once_with(
259
+            {}, self._helper.export_template, fake_output_template)
260
+        self._helper._fsal_hook.assert_called_once_with(
261
+            fake_basepath, self.share, self.access)
262
+        self._helper.ganesha.add_export.assert_called_once_with(
263
+            fake_export_name, fake_output_template)
264
+        self.assertEqual(None, ret)
265
+
266
+    def test_allow_access_error_invalid_share(self):
267
+        access = fake_access(access_type='notip')
268
+        self.assertRaises(exception.InvalidShareAccess,
269
+                          self._helper.allow_access, '/fakepath',
270
+                          self.share, access)
271
+
272
+    def test_deny_access(self):
273
+        ret = self._helper.deny_access('/fakepath', self.share, self.access)
274
+        self._helper.ganesha.remove_export.assert_called_once_with(
275
+            'fakename--fakeaccessid')
276
+        self.assertEqual(None, ret)

Loading…
Cancel
Save