Coverage for drivers/MooseFSSR.py : 36%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python3
2#
3# Original work copyright (C) Citrix systems
4# Modified work copyright (C) Tappest sp. z o.o., Vates SAS and XCP-ng community
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU Lesser General Public License as published
8# by the Free Software Foundation; version 2.1 only.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public License
16# along with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18#
19# MooseFSSR: Based on CEPHFSSR and FileSR, mounts MooseFS share
21from sm_typing import override
23import errno
24import os
25import syslog as _syslog
26import xmlrpc.client
27from syslog import syslog
29# careful with the import order here
30# FileSR has a circular dependency:
31# FileSR -> blktap2 -> lvutil -> EXTSR -> FileSR
32# importing in this order seems to avoid triggering the issue.
33import SR
34import SRCommand
35import FileSR
36# end of careful
37import VDI
38import cleanup
39import lock
40import util
41import xs_errors
43CAPABILITIES = ["SR_PROBE", "SR_UPDATE",
44 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH",
45 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", "VDI_MIRROR",
46 "VDI_GENERATE_CONFIG",
47 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE"]
49CONFIGURATION = [
50 ['masterhost', 'MooseFS Master Server hostname or IP address (required, e.g.: "mfsmaster.local.lan" or "10.10.10.1")'],
51 ['masterport', 'MooseFS Master Server port, default: 9421'],
52 ['rootpath', 'MooseFS path (required, e.g.: "/")'],
53 ['options', 'MooseFS Client additional options (e.g.: "mfspassword=PASSWORD,mfstimeout=300")']
54]
56DRIVER_INFO = {
57 'name': 'MooseFS VHD and QCOW2',
58 'description': 'SR plugin which stores disks as VHD and QCOW2 files on a MooseFS storage',
59 'vendor': 'Tappest sp. z o.o.',
60 'copyright': '(C) 2021 Tappest sp. z o.o.',
61 'driver_version': '1.0',
62 'required_api_version': '1.0',
63 'capabilities': CAPABILITIES,
64 'configuration': CONFIGURATION
65}
67DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True}
69# The mountpoint for the directory when performing an sr_probe. All probes
70# are guaranteed to be serialised by xapi, so this single mountpoint is fine.
71PROBE_MOUNTPOINT = os.path.join(SR.MOUNT_BASE, "probe")
74class MooseFSException(Exception):
75 def __init__(self, errstr):
76 self.errstr = errstr
79class MooseFSSR(FileSR.FileSR):
80 """MooseFS file-based storage"""
82 DRIVER_TYPE = 'moosefs'
84 @override
85 @staticmethod
86 def handles(sr_type) -> bool:
87 # fudge, because the parent class (FileSR) checks for smb to alter its behavior
88 return sr_type == MooseFSSR.DRIVER_TYPE or sr_type == 'smb'
90 @override
91 def load(self, sr_uuid) -> None:
92 if not self._is_moosefs_available(): 92 ↛ 93line 92 didn't jump to line 93, because the condition on line 92 was never true
93 raise xs_errors.XenError(
94 'SRUnavailable',
95 opterr='MooseFS Client is not installed!'
96 )
98 self.ops_exclusive = FileSR.OPS_EXCLUSIVE
99 self.lock = lock.Lock(lock.LOCK_TYPE_SR, self.uuid)
100 self.sr_vditype = SR.DEFAULT_TAP
101 self.driver_config = DRIVER_CONFIG
102 if 'masterhost' not in self.dconf: 102 ↛ 103line 102 didn't jump to line 103, because the condition on line 102 was never true
103 raise xs_errors.XenError('ConfigServerMissing')
104 self.remoteserver = self.dconf['masterhost']
105 self.rootpath = self.dconf['rootpath']
106 self.remotepath = self.rootpath
107 # if masterport is not specified, use default: 9421
108 if 'masterport' not in self.dconf: 108 ↛ 111line 108 didn't jump to line 111, because the condition on line 108 was never false
109 self.remoteport = "9421"
110 else:
111 self.remoteport = self.dconf['masterport']
112 if self.sr_ref and self.session is not None: 112 ↛ 113line 112 didn't jump to line 113, because the condition on line 112 was never true
113 self.sm_config = self.session.xenapi.SR.get_sm_config(self.sr_ref)
114 else:
115 self.sm_config = self.srcmd.params.get('sr_sm_config') or {}
117 if self.srcmd.cmd != 'sr_create': 117 ↛ 122line 117 didn't jump to line 122, because the condition on line 117 was never false
118 self.subdir = util.strtobool(self.sm_config.get('subdir'))
119 if self.subdir: 119 ↛ 120line 119 didn't jump to line 120, because the condition on line 119 was never true
120 self.remotepath = os.path.join(self.remotepath, sr_uuid)
122 self.attached = False
123 self.path = os.path.join(SR.MOUNT_BASE, sr_uuid)
124 self.mountpoint = self.path
125 self.linkpath = self.path
126 self._check_o_direct()
128 def checkmount(self):
129 return util.ioretry(lambda: ((util.pathexists(self.mountpoint) and
130 util.ismount(self.mountpoint))))
132 def mount(self, mountpoint=None):
133 """Mount MooseFS share at 'mountpoint'"""
134 if mountpoint is None: 134 ↛ 136line 134 didn't jump to line 136, because the condition on line 134 was never false
135 mountpoint = self.mountpoint
136 elif not util.is_string(mountpoint) or mountpoint == "":
137 raise MooseFSException("Mountpoint is not a string object")
139 try:
140 if not util.ioretry(lambda: util.isdir(mountpoint)): 140 ↛ 145line 140 didn't jump to line 145, because the condition on line 140 was never false
141 util.ioretry(lambda: util.makedirs(mountpoint)) 141 ↛ 145line 141 didn't jump to line 145
142 except util.CommandException as inst:
143 raise MooseFSException("Failed to make directory: code is %d" % inst.code)
145 try:
146 options = []
147 if 'options' in self.dconf:
148 options.append(self.dconf['options'])
149 if options:
150 options = ['-o', ','.join(options)]
151 remote = '{}:{}:{}'.format(
152 self.remoteserver, self.remoteport, self.remotepath
153 )
154 command = ["mount", '-t', 'moosefs', remote, mountpoint] + options
155 util.ioretry(lambda: util.pread(command), errlist=[errno.EPIPE, errno.EIO], maxretry=2, nofail=True)
156 except util.CommandException as inst:
157 syslog(_syslog.LOG_ERR, 'MooseFS mount failed ' + inst.__str__())
158 raise MooseFSException("Mount failed with return code %d" % inst.code)
160 # Sanity check to ensure that the user has at least RO access to the
161 # mounted share. Windows sharing and security settings can be tricky.
162 try:
163 util.listdir(mountpoint)
164 except util.CommandException:
165 try:
166 self.unmount(mountpoint, True)
167 except MooseFSException:
168 util.logException('MooseFSSR.unmount()')
169 raise MooseFSException("Permission denied. Please check user privileges.")
171 def unmount(self, mountpoint, rmmountpoint):
172 try:
173 util.pread(["umount", mountpoint])
174 except util.CommandException as inst:
175 raise MooseFSException("Command umount failed with return code %d" % inst.code)
176 if rmmountpoint:
177 try:
178 os.rmdir(mountpoint)
179 except OSError as inst:
180 raise MooseFSException("Command rmdir failed with error '%s'" % inst.strerror)
182 @override
183 def attach(self, sr_uuid) -> None:
184 if not self.checkmount(): 184 ↛ 185line 184 didn't jump to line 185, because the condition on line 184 was never true
185 try:
186 self.mount()
187 except MooseFSException as exc:
188 raise xs_errors.SROSError(12, exc.errstr)
189 self.attached = True
191 @override
192 def probe(self) -> str:
193 try:
194 self.mount(PROBE_MOUNTPOINT)
195 sr_list = filter(util.match_uuid, util.listdir(PROBE_MOUNTPOINT))
196 self.unmount(PROBE_MOUNTPOINT, True)
197 except (util.CommandException, xs_errors.XenError):
198 raise
199 # Create a dictionary from the SR uuids to feed SRtoXML()
200 return util.SRtoXML({sr_uuid: {} for sr_uuid in sr_list})
202 @override
203 def detach(self, sr_uuid) -> None:
204 if not self.checkmount(): 204 ↛ 206line 204 didn't jump to line 206, because the condition on line 204 was never false
205 return
206 util.SMlog("Aborting GC/coalesce")
207 cleanup.abort(sr_uuid)
208 # Change directory to avoid unmount conflicts
209 os.chdir(SR.MOUNT_BASE)
210 self.unmount(self.mountpoint, True)
211 self.attached = False
213 @override
214 def create(self, sr_uuid, size) -> None:
215 if self.checkmount():
216 raise xs_errors.SROSError(113, 'MooseFS mount point already attached')
218 assert self.remotepath == self.rootpath
219 try:
220 self.mount()
221 except MooseFSException as exc:
222 # noinspection PyBroadException
223 try:
224 os.rmdir(self.mountpoint)
225 except:
226 # we have no recovery strategy
227 pass
228 raise xs_errors.SROSError(111, "MooseFS mount error [opterr=%s]" % exc.errstr)
230 try:
231 subdir = self.sm_config.get('subdir')
232 if subdir is None:
233 self.subdir = True
234 else:
235 self.subdir = util.strtobool(subdir)
237 self.sm_config['subdir'] = str(self.subdir)
238 self.session.xenapi.SR.set_sm_config(self.sr_ref, self.sm_config)
240 if not self.subdir:
241 return
243 subdir = os.path.join(self.mountpoint, sr_uuid)
244 if util.ioretry(lambda: util.pathexists(subdir)):
245 if util.ioretry(lambda: util.isdir(subdir)):
246 raise xs_errors.XenError('SRExists')
247 else:
248 try:
249 util.ioretry(lambda: util.makedirs(subdir))
250 except util.CommandException as e:
251 if e.code != errno.EEXIST:
252 raise MooseFSException(
253 'Failed to create SR subdir: {}'.format(e)
254 )
255 finally:
256 self.detach(sr_uuid)
258 @override
259 def delete(self, sr_uuid) -> None:
260 # try to remove/delete non VDI contents first
261 super(MooseFSSR, self).delete(sr_uuid)
262 try:
263 if self.checkmount():
264 self.detach(sr_uuid)
266 if self.subdir:
267 # Mount using rootpath (<root>) instead of <root>/<sr_uuid>.
268 self.remotepath = self.rootpath
269 self.attach(sr_uuid)
270 subdir = os.path.join(self.mountpoint, sr_uuid)
271 if util.ioretry(lambda: util.pathexists(subdir)):
272 util.ioretry(lambda: os.rmdir(subdir))
273 self.detach(sr_uuid)
274 except util.CommandException as inst:
275 self.detach(sr_uuid)
276 if inst.code != errno.ENOENT:
277 raise xs_errors.SROSError(114, "Failed to remove MooseFS mount point")
279 @override
280 def vdi(self, uuid) -> VDI.VDI:
281 return MooseFSFileVDI(self, uuid)
283 @staticmethod
284 def _is_moosefs_available():
285 return util.find_executable('mfsmount')
287class MooseFSFileVDI(FileSR.FileVDI):
288 @override
289 def attach(self, sr_uuid, vdi_uuid) -> str:
290 if not hasattr(self, 'xenstore_data'):
291 self.xenstore_data = {}
293 self.xenstore_data['storage-type'] = MooseFSSR.DRIVER_TYPE
295 return super(MooseFSFileVDI, self).attach(sr_uuid, vdi_uuid)
297 @override
298 def generate_config(self, sr_uuid, vdi_uuid) -> str:
299 util.SMlog("MooseFSFileVDI.generate_config")
300 if not util.pathexists(self.path):
301 raise xs_errors.XenError('VDIUnavailable')
302 resp = {'device_config': self.sr.dconf,
303 'sr_uuid': sr_uuid,
304 'vdi_uuid': vdi_uuid,
305 'sr_sm_config': self.sr.sm_config,
306 'command': 'vdi_attach_from_config'}
307 # Return the 'config' encoded within a normal XMLRPC response so that
308 # we can use the regular response/error parsing code.
309 config = xmlrpc.client.dumps(tuple([resp]), "vdi_attach_from_config")
310 return xmlrpc.client.dumps((config,), "", True)
312 @override
313 def attach_from_config(self, sr_uuid, vdi_uuid) -> str:
314 try:
315 if not util.pathexists(self.sr.path):
316 return self.sr.attach(sr_uuid)
317 except:
318 util.logException("MooseFSFileVDI.attach_from_config")
319 raise xs_errors.XenError('SRUnavailable',
320 opterr='Unable to attach from config')
321 return ''
323if __name__ == '__main__': 323 ↛ 324line 323 didn't jump to line 324, because the condition on line 323 was never true
324 SRCommand.run(MooseFSSR, DRIVER_INFO)
325else:
326 SR.registerSR(MooseFSSR)