Coverage for manila/share/drivers/glusterfs/layout_directory.py: 100%

132 statements  

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

1# Copyright (c) 2015 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"""GlusterFS directory mapped share layout.""" 

17 

18import math 

19import os 

20 

21from defusedxml import ElementTree as etree 

22from oslo_config import cfg 

23from oslo_log import log 

24 

25from manila import exception 

26from manila.i18n import _ 

27from manila.privsep import os as privsep_os 

28from manila.share.drivers.glusterfs import common 

29from manila.share.drivers.glusterfs import layout 

30from manila import utils 

31 

32LOG = log.getLogger(__name__) 

33 

34glusterfs_directory_mapped_opts = [ 

35 cfg.StrOpt('glusterfs_target', 

36 help='Specifies the GlusterFS volume to be mounted on the ' 

37 'Manila host. It is of the form ' 

38 '[remoteuser@]<volserver>:<volid>.'), 

39 cfg.StrOpt('glusterfs_mount_point_base', 

40 default='$state_path/mnt', 

41 help='Base directory containing mount points for Gluster ' 

42 'volumes.'), 

43] 

44 

45CONF = cfg.CONF 

46CONF.register_opts(glusterfs_directory_mapped_opts) 

47 

48 

49class GlusterfsDirectoryMappedLayout(layout.GlusterfsShareLayoutBase): 

50 

51 def __init__(self, driver, *args, **kwargs): 

52 super(GlusterfsDirectoryMappedLayout, self).__init__( 

53 driver, *args, **kwargs) 

54 self.configuration.append_config_values( 

55 common.glusterfs_common_opts) 

56 self.configuration.append_config_values( 

57 glusterfs_directory_mapped_opts) 

58 

59 def _glustermanager(self, gluster_address): 

60 """Create GlusterManager object for gluster_address.""" 

61 

62 return common.GlusterManager( 

63 gluster_address, self.driver._execute, 

64 self.configuration.glusterfs_path_to_private_key, 

65 self.configuration.glusterfs_server_password, 

66 requires={'volume': True}) 

67 

68 def do_setup(self, context): 

69 """Prepares the backend and appropriate NAS helpers.""" 

70 if not self.configuration.glusterfs_target: 

71 raise exception.GlusterfsException( 

72 _('glusterfs_target configuration that specifies the GlusterFS' 

73 ' volume to be mounted on the Manila host is not set.')) 

74 self.gluster_manager = self._glustermanager( 

75 self.configuration.glusterfs_target) 

76 self.gluster_manager.check_gluster_version( 

77 self.driver.GLUSTERFS_VERSION_MIN) 

78 self._check_mount_glusterfs() 

79 

80 # enable quota options of a GlusteFS volume to allow 

81 # creation of shares of specific size 

82 args = ('volume', 'quota', self.gluster_manager.volume, 'enable') 

83 try: 

84 self.gluster_manager.gluster_call(*args) 

85 except exception.GlusterfsException: 

86 if (self.gluster_manager. 

87 get_vol_option('features.quota')) != 'on': 

88 LOG.exception("Error in tuning GlusterFS volume to enable " 

89 "creation of shares of specific size.") 

90 raise 

91 

92 self._ensure_gluster_vol_mounted() 

93 

94 def _share_manager(self, share): 

95 comp_path = self.gluster_manager.components.copy() 

96 comp_path.update({'path': '/' + share['name']}) 

97 return self._glustermanager(comp_path) 

98 

99 def _get_mount_point_for_gluster_vol(self): 

100 """Return mount point for the GlusterFS volume.""" 

101 return os.path.join(self.configuration.glusterfs_mount_point_base, 

102 self.gluster_manager.volume) 

103 

104 def _ensure_gluster_vol_mounted(self): 

105 """Ensure GlusterFS volume is native-mounted on Manila host.""" 

106 mount_path = self._get_mount_point_for_gluster_vol() 

107 try: 

108 common._mount_gluster_vol(self.driver._execute, 

109 self.gluster_manager.export, mount_path, 

110 ensure=True) 

111 except exception.GlusterfsException: 

112 LOG.exception('Could not mount the Gluster volume %s', 

113 self.gluster_manager.volume) 

114 raise 

115 

116 def _get_local_share_path(self, share): 

117 """Determine mount path of the GlusterFS volume in the Manila host.""" 

118 local_vol_path = self._get_mount_point_for_gluster_vol() 

119 if not os.access(local_vol_path, os.R_OK): 

120 raise exception.GlusterfsException('share path %s does not exist' % 

121 local_vol_path) 

122 return os.path.join(local_vol_path, share['name']) 

123 

124 def _update_share_stats(self): 

125 """Retrieve stats info from the GlusterFS volume.""" 

126 

127 # sanity check for gluster ctl mount 

128 smpb = os.stat(self.configuration.glusterfs_mount_point_base) 

129 smp = os.stat(self._get_mount_point_for_gluster_vol()) 

130 if smpb.st_dev == smp.st_dev: 

131 raise exception.GlusterfsException( 

132 _("GlusterFS control mount is not available") 

133 ) 

134 smpv = os.statvfs(self._get_mount_point_for_gluster_vol()) 

135 

136 return {'total_capacity_gb': (smpv.f_blocks * smpv.f_frsize) >> 30, 

137 'free_capacity_gb': (smpv.f_bavail * smpv.f_frsize) >> 30} 

138 

139 def create_share(self, ctx, share, share_server=None): 

140 """Create a sub-directory/share in the GlusterFS volume.""" 

141 # probe into getting a NAS protocol helper for the share in order 

142 # to facilitate early detection of unsupported protocol type 

143 local_share_path = self._get_local_share_path(share) 

144 

145 try: 

146 privsep_os.mkdir(local_share_path) 

147 self._set_directory_quota(share, share['size']) 

148 except Exception as exc: 

149 if isinstance(exc, exception.ProcessExecutionError): 

150 exc = exception.GlusterfsException(exc) 

151 if isinstance(exc, exception.GlusterfsException): 

152 self._cleanup_create_share(local_share_path, share['name']) 

153 LOG.error('Unable to create share %s', share['name']) 

154 raise exc 

155 

156 comp_share = self.gluster_manager.components.copy() 

157 comp_share['path'] = '/' + share['name'] 

158 export_location = self.driver._setup_via_manager( 

159 {'share': share, 

160 'manager': self._glustermanager(comp_share)}) 

161 

162 return export_location 

163 

164 def _cleanup_create_share(self, share_path, share_name): 

165 """Cleanup share that errored out during its creation.""" 

166 if os.path.exists(share_path): 

167 try: 

168 privsep_os.recursive_forced_rm(share_path) 

169 except exception.ProcessExecutionError as exc: 

170 LOG.error('Cannot cleanup share, %s, that errored out ' 

171 'during its creation, but exists in GlusterFS ' 

172 'volume.', share_name) 

173 raise exception.GlusterfsException(exc) 

174 

175 def delete_share(self, context, share, share_server=None): 

176 """Remove a sub-directory/share from the GlusterFS volume.""" 

177 local_share_path = self._get_local_share_path(share) 

178 try: 

179 privsep_os.recursive_forced_rm(local_share_path) 

180 except exception.ProcessExecutionError: 

181 LOG.exception('Unable to delete share %s', share['name']) 

182 raise 

183 

184 def ensure_share(self, context, share, share_server=None): 

185 pass 

186 

187 def create_share_from_snapshot(self, context, share, snapshot, 

188 share_server=None, parent_share=None): 

189 raise NotImplementedError 

190 

191 def create_snapshot(self, context, snapshot, share_server=None): 

192 raise NotImplementedError 

193 

194 def delete_snapshot(self, context, snapshot, share_server=None): 

195 raise NotImplementedError 

196 

197 def manage_existing(self, share, driver_options): 

198 raise NotImplementedError 

199 

200 def unmanage(self, share): 

201 raise NotImplementedError 

202 

203 def extend_share(self, share, new_size, share_server=None): 

204 """Extend a sub-directory/share in the GlusterFS volume.""" 

205 self._set_directory_quota(share, new_size) 

206 

207 def shrink_share(self, share, new_size, share_server=None): 

208 """Shrink a sub-directory/share in the GlusterFS volume.""" 

209 usage = self._get_directory_usage(share) 

210 consumed_limit = int(math.ceil(usage)) 

211 if consumed_limit > new_size: 

212 raise exception.ShareShrinkingPossibleDataLoss( 

213 share_id=share['id']) 

214 

215 self._set_directory_quota(share, new_size) 

216 

217 def _set_directory_quota(self, share, new_size): 

218 sizestr = str(new_size) + 'GB' 

219 share_dir = '/' + share['name'] 

220 

221 args = ('volume', 'quota', self.gluster_manager.volume, 

222 'limit-usage', share_dir, sizestr) 

223 

224 try: 

225 self.gluster_manager.gluster_call(*args) 

226 except exception.GlusterfsException: 

227 LOG.error('Unable to set quota share %s', share['name']) 

228 raise 

229 

230 def _get_directory_usage(self, share): 

231 share_dir = '/' + share['name'] 

232 

233 args = ('--xml', 'volume', 'quota', self.gluster_manager.volume, 

234 'list', share_dir) 

235 

236 try: 

237 out, err = self.gluster_manager.gluster_call(*args) 

238 except exception.GlusterfsException: 

239 LOG.error('Unable to get quota share %s', share['name']) 

240 raise 

241 

242 volxml = etree.fromstring(out) 

243 usage_byte = volxml.find('./volQuota/limit/used_space').text 

244 usage = utils.translate_string_size_to_float(usage_byte) 

245 

246 return usage