Coverage for manila/share/drivers/zfsonlinux/utils.py: 99%

164 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright 2016 Mirantis 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""" 

17Module for storing ZFSonLinux driver utility stuff such as: 

18 - Common ZFS code 

19 - Share helpers 

20""" 

21 

22# TODO(vponomaryov): add support of SaMBa 

23 

24import abc 

25 

26from oslo_log import log 

27 

28from manila.common import constants 

29from manila import exception 

30from manila.i18n import _ 

31from manila.share import driver 

32from manila.share.drivers.ganesha import utils as ganesha_utils 

33from manila import utils 

34 

35LOG = log.getLogger(__name__) 

36 

37 

38def zfs_dataset_synchronized(f): 

39 

40 def wrapped_func(self, *args, **kwargs): 

41 key = "zfs-dataset-%s" % args[0] 

42 

43 @utils.synchronized(key, external=True) 

44 def source_func(self, *args, **kwargs): 

45 return f(self, *args, **kwargs) 

46 

47 return source_func(self, *args, **kwargs) 

48 

49 return wrapped_func 

50 

51 

52def get_remote_shell_executor( 

53 ip, port, conn_timeout, login=None, password=None, privatekey=None, 

54 max_size=10): 

55 return ganesha_utils.SSHExecutor( 

56 ip=ip, 

57 port=port, 

58 conn_timeout=conn_timeout, 

59 login=login, 

60 password=password, 

61 privatekey=privatekey, 

62 max_size=max_size, 

63 ) 

64 

65 

66class ExecuteMixin(driver.ExecuteMixin): 

67 

68 def init_execute_mixin(self, *args, **kwargs): 

69 """Init method for mixin called in the end of driver's __init__().""" 

70 super(ExecuteMixin, self).init_execute_mixin(*args, **kwargs) 

71 if self.configuration.zfs_use_ssh: 

72 self.ssh_executor = get_remote_shell_executor( 

73 ip=self.configuration.zfs_service_ip, 

74 port=22, 

75 conn_timeout=self.configuration.ssh_conn_timeout, 

76 login=self.configuration.zfs_ssh_username, 

77 password=self.configuration.zfs_ssh_user_password, 

78 privatekey=self.configuration.zfs_ssh_private_key_path, 

79 max_size=10, 

80 ) 

81 else: 

82 self.ssh_executor = None 

83 

84 def execute(self, *cmd, **kwargs): 

85 """Common interface for running shell commands.""" 

86 if kwargs.get('executor'): 

87 executor = kwargs.get('executor') 

88 elif self.ssh_executor: 

89 executor = self.ssh_executor 

90 else: 

91 executor = self._execute 

92 kwargs.pop('executor', None) 

93 if cmd[0] == 'sudo': 

94 kwargs['run_as_root'] = True 

95 cmd = cmd[1:] 

96 return executor(*cmd, **kwargs) 

97 

98 @utils.retry(retry_param=exception.ProcessExecutionError, 

99 interval=5, retries=36, backoff_rate=1) 

100 def execute_with_retry(self, *cmd, **kwargs): 

101 """Retry wrapper over common shell interface.""" 

102 try: 

103 return self.execute(*cmd, **kwargs) 

104 except exception.ProcessExecutionError as e: 

105 LOG.warning("Failed to run command, got error: %s", e) 

106 raise 

107 

108 def _get_option(self, resource_name, option_name, pool_level=False, 

109 **kwargs): 

110 """Returns value of requested zpool or zfs dataset option.""" 

111 app = 'zpool' if pool_level else 'zfs' 

112 

113 out, err = self.execute( 

114 'sudo', app, 'get', option_name, resource_name, **kwargs) 

115 

116 data = self.parse_zfs_answer(out) 

117 option = data[0]['VALUE'] 

118 msg_payload = {'option': option_name, 'value': option} 

119 LOG.debug("ZFS option %(option)s's value is %(value)s.", msg_payload) 

120 return option 

121 

122 def parse_zfs_answer(self, string): 

123 """Returns list of dicts with data returned by ZFS shell commands.""" 

124 lines = string.split('\n') 

125 if len(lines) < 2: 

126 return [] 

127 keys = list(filter(None, lines[0].split(' '))) 

128 data = [] 

129 for line in lines[1:]: 

130 values = list(filter(None, line.split(' '))) 

131 if not values: 

132 continue 

133 data.append(dict(zip(keys, values))) 

134 return data 

135 

136 def get_zpool_option(self, zpool_name, option_name, **kwargs): 

137 """Returns value of requested zpool option.""" 

138 return self._get_option(zpool_name, option_name, True, **kwargs) 

139 

140 def get_zfs_option(self, dataset_name, option_name, **kwargs): 

141 """Returns value of requested zfs dataset option.""" 

142 return self._get_option(dataset_name, option_name, False, **kwargs) 

143 

144 def zfs(self, *cmd, **kwargs): 

145 """ZFS shell commands executor.""" 

146 return self.execute('sudo', 'zfs', *cmd, **kwargs) 

147 

148 def zfs_with_retry(self, *cmd, **kwargs): 

149 """ZFS shell commands executor.""" 

150 return self.execute_with_retry('sudo', 'zfs', *cmd, **kwargs) 

151 

152 

153class NASHelperBase(metaclass=abc.ABCMeta): 

154 """Base class for share helpers of 'ZFS on Linux' driver.""" 

155 

156 def __init__(self, configuration): 

157 """Init share helper. 

158 

159 :param configuration: share driver 'configuration' instance 

160 :return: share helper instance. 

161 """ 

162 self.configuration = configuration 

163 self.init_execute_mixin() # pylint: disable=no-member 

164 self.verify_setup() 

165 

166 @abc.abstractmethod 

167 def verify_setup(self): 

168 """Performs checks for required stuff.""" 

169 

170 @abc.abstractmethod 

171 def create_exports(self, dataset_name, executor): 

172 """Creates share exports.""" 

173 

174 @abc.abstractmethod 

175 def get_exports(self, dataset_name, service, executor): 

176 """Gets/reads share exports.""" 

177 

178 @abc.abstractmethod 

179 def remove_exports(self, dataset_name, executor): 

180 """Removes share exports.""" 

181 

182 @abc.abstractmethod 

183 def update_access(self, dataset_name, access_rules, add_rules, 

184 delete_rules, executor): 

185 """Update access rules for specified ZFS dataset.""" 

186 

187 

188class NFSviaZFSHelper(ExecuteMixin, NASHelperBase): 

189 """Helper class for handling ZFS datasets as NFS shares. 

190 

191 Kernel and Fuse versions of ZFS have different syntax for setting up access 

192 rules, and this Helper designed to satisfy both making autodetection. 

193 """ 

194 

195 @property 

196 def is_kernel_version(self): 

197 """Says whether Kernel version of ZFS is used or not.""" 

198 if not hasattr(self, '_is_kernel_version'): 

199 try: 

200 self.execute('modinfo', 'zfs') 

201 self._is_kernel_version = True 

202 except exception.ProcessExecutionError as e: 

203 LOG.info( 

204 "Looks like ZFS kernel module is absent. " 

205 "Assuming FUSE version is installed. Error: %s", e) 

206 self._is_kernel_version = False 

207 return self._is_kernel_version 

208 

209 def verify_setup(self): 

210 """Performs checks for required stuff.""" 

211 out, err = self.execute('which', 'exportfs') 

212 if not out: 

213 raise exception.ZFSonLinuxException( 

214 msg=_("Utility 'exportfs' is not installed.")) 

215 try: 

216 self.execute('sudo', 'exportfs') 

217 except exception.ProcessExecutionError: 

218 LOG.exception("Call of 'exportfs' utility returned error.") 

219 raise 

220 

221 # Init that class instance attribute on start of manila-share service 

222 self.is_kernel_version 

223 

224 def create_exports(self, dataset_name, executor=None): 

225 """Creates NFS share exports for given ZFS dataset.""" 

226 return self.get_exports(dataset_name, executor=executor) 

227 

228 def get_exports(self, dataset_name, executor=None): 

229 """Gets/reads NFS share export for given ZFS dataset.""" 

230 mountpoint = self.get_zfs_option( 

231 dataset_name, 'mountpoint', executor=executor) 

232 return [ 

233 { 

234 "path": "%(ip)s:%(mp)s" % {"ip": ip, "mp": mountpoint}, 

235 "metadata": { 

236 }, 

237 "is_admin_only": is_admin_only, 

238 } for ip, is_admin_only in ( 

239 (self.configuration.zfs_share_export_ip, False), 

240 (self.configuration.zfs_service_ip, True)) 

241 ] 

242 

243 @zfs_dataset_synchronized 

244 def remove_exports(self, dataset_name, executor=None): 

245 """Removes NFS share exports for given ZFS dataset.""" 

246 sharenfs = self.get_zfs_option( 

247 dataset_name, 'sharenfs', executor=executor) 

248 if sharenfs == 'off': 

249 return 

250 self.zfs("set", "sharenfs=off", dataset_name, executor=executor) 

251 

252 def _get_parsed_access_to(self, access_to): 

253 netmask = utils.cidr_to_netmask(access_to) 

254 if netmask == '255.255.255.255': 

255 return access_to.split('/')[0] 

256 return access_to.split('/')[0] + '/' + netmask 

257 

258 @zfs_dataset_synchronized 

259 def update_access(self, dataset_name, access_rules, add_rules, 

260 delete_rules, make_all_ro=False, executor=None): 

261 """Update access rules for given ZFS dataset exported as NFS share.""" 

262 rw_rules = [] 

263 ro_rules = [] 

264 for rule in access_rules: 

265 if rule['access_type'].lower() != 'ip': 

266 msg = _("Only IP access type allowed for NFS protocol.") 

267 raise exception.InvalidShareAccess(reason=msg) 

268 if (rule['access_level'] == constants.ACCESS_LEVEL_RW and 

269 not make_all_ro): 

270 rw_rules.append(self._get_parsed_access_to(rule['access_to'])) 

271 elif (rule['access_level'] in (constants.ACCESS_LEVEL_RW, 

272 constants.ACCESS_LEVEL_RO)): 

273 ro_rules.append(self._get_parsed_access_to(rule['access_to'])) 

274 else: 

275 msg = _("Unsupported access level provided - " 

276 "%s.") % rule['access_level'] 

277 raise exception.InvalidShareAccess(reason=msg) 

278 

279 rules = [] 

280 if self.is_kernel_version: 

281 if rw_rules: 

282 rules.append( 

283 "rw=%s,no_root_squash" % ":".join(rw_rules)) 

284 if ro_rules: 

285 rules.append("ro=%s,no_root_squash" % ":".join(ro_rules)) 

286 rules_str = "sharenfs=" + (','.join(rules) or 'off') 

287 else: 

288 for rule in rw_rules: 

289 rules.append("%s:rw,no_root_squash" % rule) 

290 for rule in ro_rules: 

291 rules.append("%s:ro,no_root_squash" % rule) 

292 rules_str = "sharenfs=" + (' '.join(rules) or 'off') 

293 

294 out, err = self.zfs( 

295 'list', '-r', dataset_name.split('/')[0], executor=executor) 

296 data = self.parse_zfs_answer(out) 

297 for datum in data: 

298 if datum['NAME'] == dataset_name: 298 ↛ 297line 298 didn't jump to line 297 because the condition on line 298 was always true

299 self.zfs("set", rules_str, dataset_name) 

300 break 

301 else: 

302 LOG.warning( 

303 "Dataset with '%(name)s' NAME is absent on backend. " 

304 "Access rules were not applied.", {'name': dataset_name}) 

305 

306 # NOTE(vponomaryov): Setting of ZFS share options does not remove rules 

307 # that were added and then removed. So, remove them explicitly. 

308 if delete_rules and access_rules: 

309 mountpoint = self.get_zfs_option(dataset_name, 'mountpoint') 

310 for rule in delete_rules: 

311 if rule['access_type'].lower() != 'ip': 

312 continue 

313 access_to = self._get_parsed_access_to(rule['access_to']) 

314 export_location = access_to + ':' + mountpoint 

315 self.execute( 

316 'sudo', 'exportfs', '-u', export_location, 

317 executor=executor, 

318 )