Coverage for drivers/SMBSR.py : 62%

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 vhdutil
32from lock import Lock
33import cleanup
34import cifutils
36CAPABILITIES = ["SR_PROBE", "SR_UPDATE", "SR_CACHING",
37 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH",
38 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", "VDI_MIRROR",
39 "VDI_GENERATE_CONFIG",
40 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE", "VDI_CONFIG_CBT",
41 "VDI_ACTIVATE", "VDI_DEACTIVATE", "THIN_PROVISIONING", "VDI_READ_CACHING"]
43CONFIGURATION = [['server', 'Full path to share root on SMB server (required)'], \
44 ['username', 'The username to be used during SMB authentication'], \
45 ['password', 'The password to be used during SMB authentication']]
47DRIVER_INFO = {
48 'name': 'SMB VHD',
49 'description': 'SR plugin which stores disks as VHD files on a remote SMB filesystem',
50 'vendor': 'Citrix Systems Inc',
51 'copyright': '(C) 2015 Citrix Systems Inc',
52 'driver_version': '1.0',
53 'required_api_version': '1.0',
54 'capabilities': CAPABILITIES,
55 'configuration': CONFIGURATION
56 }
58DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True}
60# The mountpoint for the directory when performing an sr_probe. All probes
61# are guaranteed to be serialised by xapi, so this single mountpoint is fine.
62PROBE_MOUNTPOINT = os.path.join(SR.MOUNT_BASE, "probe")
65class SMBException(Exception):
66 def __init__(self, errstr):
67 self.errstr = errstr
70# server = //smb-server/vol1 - ie the export path on the SMB server
71# mountpoint = /var/run/sr-mount/SMB/<smb_server_name>/<share_name>/uuid
72# linkpath = mountpoint/uuid - path to SR directory on share
73# path = /var/run/sr-mount/uuid - symlink to SR directory on share
74class SMBSR(FileSR.SharedFileSR):
75 """SMB file-based storage repository"""
77 @override
78 @staticmethod
79 def handles(type) -> bool:
80 return type == 'smb'
82 @override
83 def load(self, sr_uuid) -> None:
84 self.ops_exclusive = FileSR.OPS_EXCLUSIVE
85 self.lock = Lock(vhdutil.LOCK_TYPE_SR, self.uuid)
86 self.sr_vditype = SR.DEFAULT_TAP
87 self.driver_config = DRIVER_CONFIG
88 if 'server' not in self.dconf: 88 ↛ 89line 88 didn't jump to line 89, because the condition on line 88 was never true
89 raise xs_errors.XenError('ConfigServerMissing')
90 self.remoteserver = self.dconf['server']
91 if self.sr_ref and self.session is not None: 91 ↛ 92line 91 didn't jump to line 92, because the condition on line 91 was never true
92 self.sm_config = self.session.xenapi.SR.get_sm_config(self.sr_ref)
93 else:
94 self.sm_config = self.srcmd.params.get('sr_sm_config') or {}
95 self.mountpoint = os.path.join(SR.MOUNT_BASE, 'SMB', self.__extract_server(), sr_uuid)
96 self.linkpath = os.path.join(self.mountpoint,
97 sr_uuid or "")
98 # Remotepath is the absolute path inside a share that is to be mounted
99 # For a SMB SR, only the root can be mounted.
100 self.remotepath = ''
101 self.path = os.path.join(SR.MOUNT_BASE, sr_uuid)
102 self._check_o_direct()
104 def checkmount(self):
105 return util.ioretry(lambda: ((util.pathexists(self.mountpoint) and \
106 util.ismount(self.mountpoint)) and \
107 util.pathexists(self.linkpath)))
109 def makeMountPoint(self, mountpoint):
110 """Mount the remote SMB export at 'mountpoint'"""
111 if mountpoint is None:
112 mountpoint = self.mountpoint
113 elif not util.is_string(mountpoint) or mountpoint == "": 113 ↛ 116line 113 didn't jump to line 116, because the condition on line 113 was never false
114 raise SMBException("mountpoint not a string object")
116 try:
117 if not util.ioretry(lambda: util.isdir(mountpoint)): 117 ↛ 122line 117 didn't jump to line 122, because the condition on line 117 was never false
118 util.ioretry(lambda: util.makedirs(mountpoint))
119 except util.CommandException as inst:
120 raise SMBException("Failed to make directory: code is %d" %
121 inst.code)
122 return mountpoint
124 def mount(self, mountpoint=None):
126 mountpoint = self.makeMountPoint(mountpoint)
128 new_env, domain = cifutils.getCIFCredentials(self.dconf, self.session)
130 options = self.getMountOptions(domain)
131 if options: 131 ↛ 134line 131 didn't jump to line 134, because the condition on line 131 was never false
132 options = ",".join(str(x) for x in options if x)
134 try:
136 util.ioretry(lambda:
137 util.pread(["mount.cifs", self.remoteserver,
138 mountpoint, "-o", options], new_env=new_env),
139 errlist=[errno.EPIPE, errno.EIO],
140 maxretry=2, nofail=True)
141 except util.CommandException as inst:
142 raise SMBException("mount failed with return code %d" % inst.code)
144 # Sanity check to ensure that the user has at least RO access to the
145 # mounted share. Windows sharing and security settings can be tricky.
146 try:
147 util.listdir(mountpoint)
148 except util.CommandException:
149 try:
150 self.unmount(mountpoint, True)
151 except SMBException:
152 util.logException('SMBSR.unmount()')
153 raise SMBException("Permission denied. "
154 "Please check user privileges.")
156 def getMountOptions(self, domain):
157 """Creates option string based on parameters provided"""
158 options = ['cache=loose',
159 'vers=3.0',
160 'actimeo=0'
161 ]
163 if domain:
164 options.append('domain=' + domain)
166 if not cifutils.containsCredentials(self.dconf): 166 ↛ 168line 166 didn't jump to line 168, because the condition on line 166 was never true
167 # No login details provided.
168 options.append('guest')
170 return options
172 def unmount(self, mountpoint, rmmountpoint):
173 """Unmount the remote SMB export at 'mountpoint'"""
174 try:
175 util.pread(["umount", mountpoint])
176 except util.CommandException as inst:
177 raise SMBException("umount failed with return code %d" % inst.code)
179 if rmmountpoint: 179 ↛ exitline 179 didn't return from function 'unmount', because the condition on line 179 was never false
180 try:
181 os.rmdir(mountpoint)
182 except OSError as inst:
183 raise SMBException("rmdir failed with error '%s'" % inst.strerror)
185 def __extract_server(self):
186 return self.remoteserver[2:].replace('\\', '/')
188 def __check_license(self):
189 """Raises an exception if SMB is not licensed."""
190 if self.session is None: 190 ↛ 191line 190 didn't jump to line 191, because the condition on line 190 was never true
191 raise xs_errors.XenError('NoSMBLicense',
192 'No session object to talk to XAPI')
193 restrictions = util.get_pool_restrictions(self.session)
194 if 'restrict_cifs' in restrictions and \ 194 ↛ 196line 194 didn't jump to line 196, because the condition on line 194 was never true
195 restrictions['restrict_cifs'] == "true":
196 raise xs_errors.XenError('NoSMBLicense')
198 @override
199 def attach(self, sr_uuid) -> None:
200 if not self.checkmount():
201 try:
202 self.mount()
203 os.symlink(self.linkpath, self.path)
204 self._check_writable()
205 self._check_hardlinks()
206 except SMBException as exc:
207 raise xs_errors.XenError('SMBMount', opterr=exc.errstr)
208 except:
209 if util.pathexists(self.path):
210 os.unlink(self.path)
211 if self.checkmount():
212 self.unmount(self.mountpoint, True)
213 raise
215 self.attached = True
217 @override
218 def probe(self) -> str:
219 err = "SMBMount"
220 try:
221 self.mount(PROBE_MOUNTPOINT)
222 sr_list = filter(util.match_uuid, util.listdir(PROBE_MOUNTPOINT))
223 err = "SMBUnMount"
224 self.unmount(PROBE_MOUNTPOINT, True)
225 except SMBException as inst:
226 # pylint: disable=used-before-assignment
227 raise xs_errors.XenError(err, opterr=inst.errstr)
228 except (util.CommandException, xs_errors.XenError):
229 raise
230 # Create a dictionary from the SR uuids to feed SRtoXML()
231 return util.SRtoXML({sr_uuid: {} for sr_uuid in sr_list})
233 @override
234 def detach(self, sr_uuid) -> None:
235 """Detach the SR: Unmounts and removes the mountpoint"""
236 if not self.checkmount():
237 return
238 util.SMlog("Aborting GC/coalesce")
239 cleanup.abort(self.uuid)
241 # Change directory to avoid unmount conflicts
242 os.chdir(SR.MOUNT_BASE)
244 try:
245 self.unmount(self.mountpoint, True)
246 os.unlink(self.path)
247 except SMBException as exc:
248 raise xs_errors.XenError('SMBUnMount', opterr=exc.errstr)
250 self.attached = False
252 @override
253 def create(self, sr_uuid, size) -> None:
254 self.__check_license()
256 if self.checkmount(): 256 ↛ 257line 256 didn't jump to line 257, because the condition on line 256 was never true
257 raise xs_errors.XenError('SMBAttached')
259 try:
260 self.mount()
261 except SMBException as exc:
262 try:
263 os.rmdir(self.mountpoint)
264 except:
265 pass
266 raise xs_errors.XenError('SMBMount', opterr=exc.errstr)
268 if util.ioretry(lambda: util.pathexists(self.linkpath)): 268 ↛ 269line 268 didn't jump to line 269, because the condition on line 268 was never true
269 if len(util.ioretry(lambda: util.listdir(self.linkpath))) != 0:
270 self.detach(sr_uuid)
271 raise xs_errors.XenError('SRExists')
272 else:
273 try:
274 util.ioretry(lambda: util.makedirs(self.linkpath))
275 os.symlink(self.linkpath, self.path)
276 except util.CommandException as inst:
277 if inst.code != errno.EEXIST: 277 ↛ 293line 277 didn't jump to line 293, because the condition on line 277 was never false
278 try:
279 self.unmount(self.mountpoint, True)
280 except SMBException:
281 util.logException('SMBSR.unmount()')
283 if inst.code in [errno.EROFS, errno.EPERM, errno.EACCES]:
284 raise xs_errors.XenError(
285 'SharedFileSystemNoWrite',
286 opterr='remote filesystem is read-only error is %d'
287 % inst.code) from inst
288 else:
289 raise xs_errors.XenError(
290 'SMBCreate',
291 opterr="remote directory creation error: {}"
292 .format(os.strerror(inst.code))) from inst
293 self.detach(sr_uuid)
295 @override
296 def delete(self, sr_uuid) -> None:
297 # try to remove/delete non VDI contents first
298 super(SMBSR, self).delete(sr_uuid)
299 try:
300 if self.checkmount():
301 self.detach(sr_uuid)
303 self.mount()
304 if util.ioretry(lambda: util.pathexists(self.linkpath)):
305 util.ioretry(lambda: os.rmdir(self.linkpath))
306 self.unmount(self.mountpoint, True)
307 except util.CommandException as inst:
308 self.detach(sr_uuid)
309 if inst.code != errno.ENOENT:
310 raise xs_errors.XenError('SMBDelete')
312 @override
313 def vdi(self, uuid) -> VDI.VDI:
314 return SMBFileVDI(self, uuid)
317class SMBFileVDI(FileSR.FileVDI):
318 @override
319 def attach(self, sr_uuid, vdi_uuid) -> str:
320 if not hasattr(self, 'xenstore_data'):
321 self.xenstore_data = {}
323 self.xenstore_data["storage-type"] = "smb"
325 return super(SMBFileVDI, self).attach(sr_uuid, vdi_uuid)
327 @override
328 def generate_config(self, sr_uuid, vdi_uuid) -> str:
329 util.SMlog("SMBFileVDI.generate_config")
330 if not util.pathexists(self.path):
331 raise xs_errors.XenError('VDIUnavailable')
332 resp = {}
333 resp['device_config'] = self.sr.dconf
334 resp['sr_uuid'] = sr_uuid
335 resp['vdi_uuid'] = vdi_uuid
336 resp['sr_sm_config'] = self.sr.sm_config
337 resp['command'] = 'vdi_attach_from_config'
338 # Return the 'config' encoded within a normal XMLRPC response so that
339 # we can use the regular response/error parsing code.
340 config = xmlrpc.client.dumps(tuple([resp]), "vdi_attach_from_config")
341 return xmlrpc.client.dumps((config, ), "", True)
343 @override
344 def attach_from_config(self, sr_uuid, vdi_uuid) -> str:
345 """Used for HA State-file only. Will not just attach the VDI but
346 also start a tapdisk on the file"""
347 util.SMlog("SMBFileVDI.attach_from_config")
348 try:
349 if not util.pathexists(self.sr.path):
350 return self.sr.attach(sr_uuid)
351 except:
352 util.logException("SMBFileVDI.attach_from_config")
353 raise xs_errors.XenError('SRUnavailable', \
354 opterr='Unable to attach from config')
355 return ''
358if __name__ == '__main__': 358 ↛ 359line 358 didn't jump to line 359, because the condition on line 358 was never true
359 SRCommand.run(SMBSR, DRIVER_INFO)
360else:
361 SR.registerSR(SMBSR)
362#