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 util
40import vhdutil
41import xs_errors
42from lock import Lock
44CAPABILITIES = ["SR_PROBE", "SR_UPDATE",
45 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH",
46 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", "VDI_MIRROR",
47 "VDI_GENERATE_CONFIG",
48 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE"]
50CONFIGURATION = [
51 ['masterhost', 'MooseFS Master Server hostname or IP address (required, e.g.: "mfsmaster.local.lan" or "10.10.10.1")'],
52 ['masterport', 'MooseFS Master Server port, default: 9421'],
53 ['rootpath', 'MooseFS path (required, e.g.: "/")'],
54 ['options', 'MooseFS Client additional options (e.g.: "mfspassword=PASSWORD,mfstimeout=300")']
55]
57DRIVER_INFO = {
58 'name': 'MooseFS VHD',
59 'description': 'SR plugin which stores disks as VHD files on a MooseFS storage',
60 'vendor': 'Tappest sp. z o.o.',
61 'copyright': '(C) 2021 Tappest sp. z o.o.',
62 'driver_version': '1.0',
63 'required_api_version': '1.0',
64 'capabilities': CAPABILITIES,
65 'configuration': CONFIGURATION
66}
68DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True}
70# The mountpoint for the directory when performing an sr_probe. All probes
71# are guaranteed to be serialised by xapi, so this single mountpoint is fine.
72PROBE_MOUNTPOINT = os.path.join(SR.MOUNT_BASE, "probe")
75class MooseFSException(Exception):
76 def __init__(self, errstr):
77 self.errstr = errstr
80class MooseFSSR(FileSR.FileSR):
81 """MooseFS file-based storage"""
83 DRIVER_TYPE = 'moosefs'
85 @override
86 @staticmethod
87 def handles(sr_type) -> bool:
88 # fudge, because the parent class (FileSR) checks for smb to alter its behavior
89 return sr_type == MooseFSSR.DRIVER_TYPE or sr_type == 'smb'
91 @override
92 def load(self, sr_uuid) -> None:
93 if not self._is_moosefs_available(): 93 ↛ 94line 93 didn't jump to line 94, because the condition on line 93 was never true
94 raise xs_errors.XenError(
95 'SRUnavailable',
96 opterr='MooseFS Client is not installed!'
97 )
99 self.ops_exclusive = FileSR.OPS_EXCLUSIVE
100 self.lock = Lock(vhdutil.LOCK_TYPE_SR, self.uuid)
101 self.sr_vditype = SR.DEFAULT_TAP
102 self.driver_config = DRIVER_CONFIG
103 if 'masterhost' not in self.dconf: 103 ↛ 104line 103 didn't jump to line 104, because the condition on line 103 was never true
104 raise xs_errors.XenError('ConfigServerMissing')
105 self.remoteserver = self.dconf['masterhost']
106 self.rootpath = self.dconf['rootpath']
107 self.remotepath = self.rootpath
108 # if masterport is not specified, use default: 9421
109 if 'masterport' not in self.dconf: 109 ↛ 112line 109 didn't jump to line 112, because the condition on line 109 was never false
110 self.remoteport = "9421"
111 else:
112 self.remoteport = self.dconf['masterport']
113 if self.sr_ref and self.session is not None: 113 ↛ 114line 113 didn't jump to line 114, because the condition on line 113 was never true
114 self.sm_config = self.session.xenapi.SR.get_sm_config(self.sr_ref)
115 else:
116 self.sm_config = self.srcmd.params.get('sr_sm_config') or {}
118 if self.srcmd.cmd != 'sr_create': 118 ↛ 123line 118 didn't jump to line 123, because the condition on line 118 was never false
119 self.subdir = util.strtobool(self.sm_config.get('subdir'))
120 if self.subdir: 120 ↛ 121line 120 didn't jump to line 121, because the condition on line 120 was never true
121 self.remotepath = os.path.join(self.remotepath, sr_uuid)
123 self.attached = False
124 self.path = os.path.join(SR.MOUNT_BASE, sr_uuid)
125 self.mountpoint = self.path
126 self.linkpath = self.path
127 self._check_o_direct()
129 def checkmount(self):
130 return util.ioretry(lambda: ((util.pathexists(self.mountpoint) and
131 util.ismount(self.mountpoint))))
133 def mount(self, mountpoint=None):
134 """Mount MooseFS share at 'mountpoint'"""
135 if mountpoint is None: 135 ↛ 137line 135 didn't jump to line 137, because the condition on line 135 was never false
136 mountpoint = self.mountpoint
137 elif not util.is_string(mountpoint) or mountpoint == "":
138 raise MooseFSException("Mountpoint is not a string object")
140 try:
141 if not util.ioretry(lambda: util.isdir(mountpoint)): 141 ↛ 146line 141 didn't jump to line 146, because the condition on line 141 was never false
142 util.ioretry(lambda: util.makedirs(mountpoint)) 142 ↛ 146line 142 didn't jump to line 146
143 except util.CommandException as inst:
144 raise MooseFSException("Failed to make directory: code is %d" % inst.code)
146 try:
147 options = []
148 if 'options' in self.dconf:
149 options.append(self.dconf['options'])
150 if options:
151 options = ['-o', ','.join(options)]
152 remote = '{}:{}:{}'.format(
153 self.remoteserver, self.remoteport, self.remotepath
154 )
155 command = ["mount", '-t', 'moosefs', remote, mountpoint] + options
156 util.ioretry(lambda: util.pread(command), errlist=[errno.EPIPE, errno.EIO], maxretry=2, nofail=True)
157 except util.CommandException as inst:
158 syslog(_syslog.LOG_ERR, 'MooseFS mount failed ' + inst.__str__())
159 raise MooseFSException("Mount failed with return code %d" % inst.code)
161 # Sanity check to ensure that the user has at least RO access to the
162 # mounted share. Windows sharing and security settings can be tricky.
163 try:
164 util.listdir(mountpoint)
165 except util.CommandException:
166 try:
167 self.unmount(mountpoint, True)
168 except MooseFSException:
169 util.logException('MooseFSSR.unmount()')
170 raise MooseFSException("Permission denied. Please check user privileges.")
172 def unmount(self, mountpoint, rmmountpoint):
173 try:
174 util.pread(["umount", mountpoint])
175 except util.CommandException as inst:
176 raise MooseFSException("Command umount failed with return code %d" % inst.code)
177 if rmmountpoint:
178 try:
179 os.rmdir(mountpoint)
180 except OSError as inst:
181 raise MooseFSException("Command rmdir failed with error '%s'" % inst.strerror)
183 @override
184 def attach(self, sr_uuid) -> None:
185 if not self.checkmount(): 185 ↛ 186line 185 didn't jump to line 186, because the condition on line 185 was never true
186 try:
187 self.mount()
188 except MooseFSException as exc:
189 raise xs_errors.SROSError(12, exc.errstr)
190 self.attached = True
192 @override
193 def probe(self) -> str:
194 try:
195 self.mount(PROBE_MOUNTPOINT)
196 sr_list = filter(util.match_uuid, util.listdir(PROBE_MOUNTPOINT))
197 self.unmount(PROBE_MOUNTPOINT, True)
198 except (util.CommandException, xs_errors.XenError):
199 raise
200 # Create a dictionary from the SR uuids to feed SRtoXML()
201 return util.SRtoXML({sr_uuid: {} for sr_uuid in sr_list})
203 @override
204 def detach(self, sr_uuid) -> None:
205 if not self.checkmount(): 205 ↛ 207line 205 didn't jump to line 207, because the condition on line 205 was never false
206 return
207 util.SMlog("Aborting GC/coalesce")
208 cleanup.abort(sr_uuid)
209 # Change directory to avoid unmount conflicts
210 os.chdir(SR.MOUNT_BASE)
211 self.unmount(self.mountpoint, True)
212 self.attached = False
214 @override
215 def create(self, sr_uuid, size) -> None:
216 if self.checkmount():
217 raise xs_errors.SROSError(113, 'MooseFS mount point already attached')
219 assert self.remotepath == self.rootpath
220 try:
221 self.mount()
222 except MooseFSException as exc:
223 # noinspection PyBroadException
224 try:
225 os.rmdir(self.mountpoint)
226 except:
227 # we have no recovery strategy
228 pass
229 raise xs_errors.SROSError(111, "MooseFS mount error [opterr=%s]" % exc.errstr)
231 try:
232 self.subdir = self.sm_config.get('subdir')
233 if self.subdir is None:
234 self.subdir = True
235 else:
236 self.subdir = util.strtobool(self.subdir)
238 self.sm_config['subdir'] = str(self.subdir)
239 self.session.xenapi.SR.set_sm_config(self.sr_ref, self.sm_config)
241 if not self.subdir:
242 return
244 subdir = os.path.join(self.mountpoint, sr_uuid)
245 if util.ioretry(lambda: util.pathexists(subdir)):
246 if util.ioretry(lambda: util.isdir(subdir)):
247 raise xs_errors.XenError('SRExists')
248 else:
249 try:
250 util.ioretry(lambda: util.makedirs(subdir))
251 except util.CommandException as e:
252 if e.code != errno.EEXIST:
253 raise MooseFSException(
254 'Failed to create SR subdir: {}'.format(e)
255 )
256 finally:
257 self.detach(sr_uuid)
259 @override
260 def delete(self, sr_uuid) -> None:
261 # try to remove/delete non VDI contents first
262 super(MooseFSSR, self).delete(sr_uuid)
263 try:
264 if self.checkmount():
265 self.detach(sr_uuid)
267 if self.subdir:
268 # Mount using rootpath (<root>) instead of <root>/<sr_uuid>.
269 self.remotepath = self.rootpath
270 self.attach(sr_uuid)
271 subdir = os.path.join(self.mountpoint, sr_uuid)
272 if util.ioretry(lambda: util.pathexists(subdir)):
273 util.ioretry(lambda: os.rmdir(subdir))
274 self.detach(sr_uuid)
275 except util.CommandException as inst:
276 self.detach(sr_uuid)
277 if inst.code != errno.ENOENT:
278 raise xs_errors.SROSError(114, "Failed to remove MooseFS mount point")
280 @override
281 def vdi(self, uuid, loadLocked=False) -> VDI.VDI:
282 return MooseFSFileVDI(self, uuid)
284 @staticmethod
285 def _is_moosefs_available():
286 return util.find_executable('mfsmount')
288class MooseFSFileVDI(FileSR.FileVDI):
289 @override
290 def attach(self, sr_uuid, vdi_uuid) -> str:
291 if not hasattr(self, 'xenstore_data'):
292 self.xenstore_data = {}
294 self.xenstore_data['storage-type'] = MooseFSSR.DRIVER_TYPE
296 return super(MooseFSFileVDI, self).attach(sr_uuid, vdi_uuid)
298 @override
299 def generate_config(self, sr_uuid, vdi_uuid) -> str:
300 util.SMlog("MooseFSFileVDI.generate_config")
301 if not util.pathexists(self.path):
302 raise xs_errors.XenError('VDIUnavailable')
303 resp = {'device_config': self.sr.dconf,
304 'sr_uuid': sr_uuid,
305 'vdi_uuid': vdi_uuid,
306 'sr_sm_config': self.sr.sm_config,
307 'command': 'vdi_attach_from_config'}
308 # Return the 'config' encoded within a normal XMLRPC response so that
309 # we can use the regular response/error parsing code.
310 config = xmlrpc.client.dumps(tuple([resp]), "vdi_attach_from_config")
311 return xmlrpc.client.dumps((config,), "", True)
313 @override
314 def attach_from_config(self, sr_uuid, vdi_uuid) -> str:
315 try:
316 if not util.pathexists(self.sr.path):
317 return self.sr.attach(sr_uuid)
318 except:
319 util.logException("MooseFSFileVDI.attach_from_config")
320 raise xs_errors.XenError('SRUnavailable',
321 opterr='Unable to attach from config')
322 return ''
324if __name__ == '__main__': 324 ↛ 325line 324 didn't jump to line 325, because the condition on line 324 was never true
325 SRCommand.run(MooseFSSR, DRIVER_INFO)
326else:
327 SR.registerSR(MooseFSSR)