Coverage for drivers/SMBSR.py : 61%
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/python3
2#
3# Copyright (C) Citrix Systems Inc.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as published
7# by the Free Software Foundation; version 2.1 only.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17#
18# SMBSR: SMB filesystem based storage repository
20from sm_typing import override
22import SR
23import SRCommand
24import VDI
25import FileSR
26import util
27import errno
28import os
29import xmlrpc.client
30import xs_errors
31import lock
32import cleanup
33import cifutils
35CAPABILITIES = ["SR_PROBE", "SR_UPDATE", "SR_CACHING",
36 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH",
37 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", "VDI_MIRROR",
38 "VDI_GENERATE_CONFIG",
39 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE", "VDI_CONFIG_CBT",
40 "VDI_ACTIVATE", "VDI_DEACTIVATE", "THIN_PROVISIONING", "VDI_READ_CACHING"]
42CONFIGURATION = [['server', 'Full path to share root on SMB server (required)'], \
43 ['username', 'The username to be used during SMB authentication'], \
44 ['password', 'The password to be used during SMB authentication']]
46DRIVER_INFO = {
47 'name': 'SMB VHD and QCOW2',
48 'description': 'SR plugin which stores disks as VHD and QCOW2 files on a remote SMB filesystem',
49 'vendor': 'Citrix Systems Inc',
50 'copyright': '(C) 2015 Citrix Systems Inc',
51 'driver_version': '1.0',
52 'required_api_version': '1.0',
53 'capabilities': CAPABILITIES,
54 'configuration': CONFIGURATION
55 }
57DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True}
59# The mountpoint for the directory when performing an sr_probe. All probes
60# are guaranteed to be serialised by xapi, so this single mountpoint is fine.
61PROBE_MOUNTPOINT = os.path.join(SR.MOUNT_BASE, "probe")
64class SMBException(Exception):
65 def __init__(self, errstr):
66 self.errstr = errstr
69# server = //smb-server/vol1 - ie the export path on the SMB server
70# mountpoint = /var/run/sr-mount/SMB/<smb_server_name>/<share_name>/uuid
71# linkpath = mountpoint/uuid - path to SR directory on share
72# path = /var/run/sr-mount/uuid - symlink to SR directory on share
73class SMBSR(FileSR.SharedFileSR):
74 """SMB file-based storage repository"""
76 @override
77 @staticmethod
78 def handles(type) -> bool:
79 return type == 'smb'
81 @override
82 def load(self, sr_uuid) -> None:
83 self.ops_exclusive = FileSR.OPS_EXCLUSIVE
84 self.lock = lock.Lock(lock.LOCK_TYPE_SR, self.uuid)
85 self.sr_vditype = SR.DEFAULT_TAP
86 self.driver_config = DRIVER_CONFIG
87 if 'server' not in self.dconf: 87 ↛ 88line 87 didn't jump to line 88, because the condition on line 87 was never true
88 raise xs_errors.XenError('ConfigServerMissing')
89 self.remoteserver = self.dconf['server']
90 if self.sr_ref and self.session is not None: 90 ↛ 91line 90 didn't jump to line 91, because the condition on line 90 was never true
91 self.sm_config = self.session.xenapi.SR.get_sm_config(self.sr_ref)
92 else:
93 self.sm_config = self.srcmd.params.get('sr_sm_config') or {}
94 self.mountpoint = os.path.join(SR.MOUNT_BASE, 'SMB', self.__extract_server(), sr_uuid)
95 self.linkpath = os.path.join(self.mountpoint,
96 sr_uuid or "")
97 # Remotepath is the absolute path inside a share that is to be mounted
98 # For a SMB SR, only the root can be mounted.
99 self.remotepath = ''
100 self.path = os.path.join(SR.MOUNT_BASE, sr_uuid)
101 self._check_o_direct()
103 def checkmount(self):
104 return util.ioretry(lambda: ((util.pathexists(self.mountpoint) and \
105 util.ismount(self.mountpoint)) and \
106 util.pathexists(self.linkpath)))
108 def makeMountPoint(self, mountpoint):
109 """Mount the remote SMB export at 'mountpoint'"""
110 if mountpoint is None:
111 mountpoint = self.mountpoint
112 elif not util.is_string(mountpoint) or mountpoint == "": 112 ↛ 115line 112 didn't jump to line 115, because the condition on line 112 was never false
113 raise SMBException("mountpoint not a string object")
115 try:
116 if not util.ioretry(lambda: util.isdir(mountpoint)): 116 ↛ 121line 116 didn't jump to line 121, because the condition on line 116 was never false
117 util.ioretry(lambda: util.makedirs(mountpoint))
118 except util.CommandException as inst:
119 raise SMBException("Failed to make directory: code is %d" %
120 inst.code)
121 return mountpoint
123 def mount(self, mountpoint=None):
125 mountpoint = self.makeMountPoint(mountpoint)
127 new_env, domain = cifutils.getCIFCredentials(self.dconf, self.session)
129 options = self.getMountOptions(domain)
130 if options: 130 ↛ 133line 130 didn't jump to line 133, because the condition on line 130 was never false
131 options = ",".join(str(x) for x in options if x)
133 try:
135 util.ioretry(lambda:
136 util.pread(["mount.cifs", self.remoteserver,
137 mountpoint, "-o", options], new_env=new_env),
138 errlist=[errno.EPIPE, errno.EIO],
139 maxretry=2, nofail=True)
140 except util.CommandException as inst:
141 raise SMBException("mount failed with return code %d" % inst.code)
143 # Sanity check to ensure that the user has at least RO access to the
144 # mounted share. Windows sharing and security settings can be tricky.
145 try:
146 util.listdir(mountpoint)
147 except util.CommandException:
148 try:
149 self.unmount(mountpoint, True)
150 except SMBException:
151 util.logException('SMBSR.unmount()')
152 raise SMBException("Permission denied. "
153 "Please check user privileges.")
155 def getMountOptions(self, domain):
156 """Creates option string based on parameters provided"""
157 options = ['cache=loose',
158 'vers=3.0',
159 'actimeo=0'
160 ]
162 if domain:
163 options.append('domain=' + domain)
165 if not cifutils.containsCredentials(self.dconf): 165 ↛ 167line 165 didn't jump to line 167, because the condition on line 165 was never true
166 # No login details provided.
167 options.append('guest')
169 return options
171 def unmount(self, mountpoint, rmmountpoint):
172 """Unmount the remote SMB export at 'mountpoint'"""
173 try:
174 util.pread(["umount", mountpoint])
175 except util.CommandException as inst:
176 raise SMBException("umount failed with return code %d" % inst.code)
178 if rmmountpoint: 178 ↛ exitline 178 didn't return from function 'unmount', because the condition on line 178 was never false
179 try:
180 os.rmdir(mountpoint)
181 except OSError as inst:
182 raise SMBException("rmdir failed with error '%s'" % inst.strerror)
184 def __extract_server(self):
185 return self.remoteserver[2:].replace('\\', '/')
187 def __check_license(self):
188 """Raises an exception if SMB is not licensed."""
189 if self.session is None: 189 ↛ 190line 189 didn't jump to line 190, because the condition on line 189 was never true
190 raise xs_errors.XenError('NoSMBLicense',
191 'No session object to talk to XAPI')
192 restrictions = util.get_pool_restrictions(self.session)
193 if 'restrict_cifs' in restrictions and \ 193 ↛ 195line 193 didn't jump to line 195, because the condition on line 193 was never true
194 restrictions['restrict_cifs'] == "true":
195 raise xs_errors.XenError('NoSMBLicense')
197 @override
198 def attach(self, sr_uuid) -> None:
199 if not self.checkmount():
200 try:
201 self.mount()
202 os.symlink(self.linkpath, self.path)
203 self._check_writable()
204 self._check_hardlinks()
205 except SMBException as exc:
206 raise xs_errors.XenError('SMBMount', opterr=exc.errstr)
207 except:
208 if util.pathexists(self.path):
209 os.unlink(self.path)
210 if self.checkmount():
211 self.unmount(self.mountpoint, True)
212 raise
214 self.attached = True
216 @override
217 def probe(self) -> str:
218 err = "SMBMount"
219 try:
220 self.mount(PROBE_MOUNTPOINT)
221 sr_list = filter(util.match_uuid, util.listdir(PROBE_MOUNTPOINT))
222 err = "SMBUnMount"
223 self.unmount(PROBE_MOUNTPOINT, True)
224 except SMBException as inst:
225 # pylint: disable=used-before-assignment
226 raise xs_errors.XenError(err, opterr=inst.errstr)
227 except (util.CommandException, xs_errors.XenError):
228 raise
229 # Create a dictionary from the SR uuids to feed SRtoXML()
230 return util.SRtoXML({sr_uuid: {} for sr_uuid in sr_list})
232 @override
233 def detach(self, sr_uuid) -> None:
234 """Detach the SR: Unmounts and removes the mountpoint"""
235 if not self.checkmount():
236 return
237 util.SMlog("Aborting GC/coalesce")
238 cleanup.abort(self.uuid)
240 # Change directory to avoid unmount conflicts
241 os.chdir(SR.MOUNT_BASE)
243 try:
244 self.unmount(self.mountpoint, True)
245 os.unlink(self.path)
246 except SMBException as exc:
247 raise xs_errors.XenError('SMBUnMount', opterr=exc.errstr)
249 self.attached = False
251 @override
252 def create(self, sr_uuid, size) -> None:
253 self.__check_license()
255 if self.checkmount(): 255 ↛ 256line 255 didn't jump to line 256, because the condition on line 255 was never true
256 raise xs_errors.XenError('SMBAttached')
258 try:
259 self.mount()
260 except SMBException as exc:
261 try:
262 os.rmdir(self.mountpoint)
263 except:
264 pass
265 raise xs_errors.XenError('SMBMount', opterr=exc.errstr)
267 if util.ioretry(lambda: util.pathexists(self.linkpath)): 267 ↛ 268line 267 didn't jump to line 268, because the condition on line 267 was never true
268 if len(util.ioretry(lambda: util.listdir(self.linkpath))) != 0:
269 self.detach(sr_uuid)
270 raise xs_errors.XenError('SRExists')
271 else:
272 try:
273 util.ioretry(lambda: util.makedirs(self.linkpath))
274 os.symlink(self.linkpath, self.path)
275 except util.CommandException as inst:
276 if inst.code != errno.EEXIST: 276 ↛ 292line 276 didn't jump to line 292, because the condition on line 276 was never false
277 try:
278 self.unmount(self.mountpoint, True)
279 except SMBException:
280 util.logException('SMBSR.unmount()')
282 if inst.code in [errno.EROFS, errno.EPERM, errno.EACCES]:
283 raise xs_errors.XenError(
284 'SharedFileSystemNoWrite',
285 opterr='remote filesystem is read-only error is %d'
286 % inst.code) from inst
287 else:
288 raise xs_errors.XenError(
289 'SMBCreate',
290 opterr="remote directory creation error: {}"
291 .format(os.strerror(inst.code))) from inst
292 self.detach(sr_uuid)
294 @override
295 def delete(self, sr_uuid) -> None:
296 # try to remove/delete non VDI contents first
297 super(SMBSR, self).delete(sr_uuid)
298 try:
299 if self.checkmount():
300 self.detach(sr_uuid)
302 self.mount()
303 if util.ioretry(lambda: util.pathexists(self.linkpath)):
304 util.ioretry(lambda: os.rmdir(self.linkpath))
305 self.unmount(self.mountpoint, True)
306 except util.CommandException as inst:
307 self.detach(sr_uuid)
308 if inst.code != errno.ENOENT:
309 raise xs_errors.XenError('SMBDelete')
311 @override
312 def vdi(self, uuid) -> VDI.VDI:
313 return SMBFileVDI(self, uuid)
316class SMBFileVDI(FileSR.FileVDI):
317 @override
318 def attach(self, sr_uuid, vdi_uuid) -> str:
319 if not hasattr(self, 'xenstore_data'):
320 self.xenstore_data = {}
322 self.xenstore_data["storage-type"] = "smb"
324 return super(SMBFileVDI, self).attach(sr_uuid, vdi_uuid)
326 @override
327 def generate_config(self, sr_uuid, vdi_uuid) -> str:
328 util.SMlog("SMBFileVDI.generate_config")
329 if not util.pathexists(self.path):
330 raise xs_errors.XenError('VDIUnavailable')
331 resp = {}
332 resp['device_config'] = self.sr.dconf
333 resp['sr_uuid'] = sr_uuid
334 resp['vdi_uuid'] = vdi_uuid
335 resp['sr_sm_config'] = self.sr.sm_config
336 resp['command'] = 'vdi_attach_from_config'
337 # Return the 'config' encoded within a normal XMLRPC response so that
338 # we can use the regular response/error parsing code.
339 config = xmlrpc.client.dumps(tuple([resp]), "vdi_attach_from_config")
340 return xmlrpc.client.dumps((config, ), "", True)
342 @override
343 def attach_from_config(self, sr_uuid, vdi_uuid) -> str:
344 """Used for HA State-file only. Will not just attach the VDI but
345 also start a tapdisk on the file"""
346 util.SMlog("SMBFileVDI.attach_from_config")
347 try:
348 if not util.pathexists(self.sr.path):
349 return self.sr.attach(sr_uuid)
350 except:
351 util.logException("SMBFileVDI.attach_from_config")
352 raise xs_errors.XenError('SRUnavailable', \
353 opterr='Unable to attach from config')
354 return ''
357if __name__ == '__main__': 357 ↛ 358line 357 didn't jump to line 358, because the condition on line 357 was never true
358 SRCommand.run(SMBSR, DRIVER_INFO)
359else:
360 SR.registerSR(SMBSR)
361#