Coverage for drivers/SMBSR.py : 60%

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