Coverage for drivers/SMBSR.py : 45%

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 ↛ 113, 112 ↛ 1172 missed branches: 1) line 112 didn't jump to line 113, because the condition on line 112 was never true, 2) line 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:
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:
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 \
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 raise xs_errors.XenError(err, opterr=inst.errstr)
220 except (util.CommandException, xs_errors.XenError):
221 raise
223 # Create a dictionary from the SR uuids to feed SRtoXML()
224 sr_dict = {sr_uuid: {} for sr_uuid in sr_list}
226 return util.SRtoXML(sr_dict)
228 def detach(self, sr_uuid):
229 """Detach the SR: Unmounts and removes the mountpoint"""
230 if not self.checkmount():
231 return
232 util.SMlog("Aborting GC/coalesce")
233 cleanup.abort(self.uuid)
235 # Change directory to avoid unmount conflicts
236 os.chdir(SR.MOUNT_BASE)
238 try:
239 self.unmount(self.mountpoint, True)
240 os.unlink(self.path)
241 except SMBException as exc:
242 raise xs_errors.XenError('SMBUnMount', opterr=exc.errstr)
244 self.attached = False
246 def create(self, sr_uuid, size):
247 self.__check_license()
249 if self.checkmount():
250 raise xs_errors.XenError('SMBAttached')
252 try:
253 self.mount()
254 except SMBException as exc:
255 try:
256 os.rmdir(self.mountpoint)
257 except:
258 pass
259 raise xs_errors.XenError('SMBMount', opterr=exc.errstr)
261 if util.ioretry(lambda: util.pathexists(self.linkpath)):
262 if len(util.ioretry(lambda: util.listdir(self.linkpath))) != 0:
263 self.detach(sr_uuid)
264 raise xs_errors.XenError('SRExists')
265 else:
266 try:
267 util.ioretry(lambda: util.makedirs(self.linkpath))
268 os.symlink(self.linkpath, self.path)
269 except util.CommandException as inst:
270 if inst.code != errno.EEXIST:
271 try:
272 self.unmount(self.mountpoint, True)
273 except SMBException:
274 util.logException('SMBSR.unmount()')
275 raise xs_errors.XenError(
276 'SMBCreate',
277 opterr="remote directory creation error: {}"
278 .format(os.strerror(inst.code))
279 )
280 self.detach(sr_uuid)
282 def delete(self, sr_uuid):
283 # try to remove/delete non VDI contents first
284 super(SMBSR, self).delete(sr_uuid)
285 try:
286 if self.checkmount():
287 self.detach(sr_uuid)
289 self.mount()
290 if util.ioretry(lambda: util.pathexists(self.linkpath)):
291 util.ioretry(lambda: os.rmdir(self.linkpath))
292 self.unmount(self.mountpoint, True)
293 except util.CommandException as inst:
294 self.detach(sr_uuid)
295 if inst.code != errno.ENOENT:
296 raise xs_errors.XenError('SMBDelete')
298 def vdi(self, uuid):
299 return SMBFileVDI(self, uuid)
302class SMBFileVDI(FileSR.FileVDI):
303 def attach(self, sr_uuid, vdi_uuid):
304 if not hasattr(self, 'xenstore_data'):
305 self.xenstore_data = {}
307 self.xenstore_data["storage-type"] = "smb"
309 return super(SMBFileVDI, self).attach(sr_uuid, vdi_uuid)
311 def generate_config(self, sr_uuid, vdi_uuid):
312 util.SMlog("SMBFileVDI.generate_config")
313 if not util.pathexists(self.path):
314 raise xs_errors.XenError('VDIUnavailable')
315 resp = {}
316 resp['device_config'] = self.sr.dconf
317 resp['sr_uuid'] = sr_uuid
318 resp['vdi_uuid'] = vdi_uuid
319 resp['sr_sm_config'] = self.sr.sm_config
320 resp['command'] = 'vdi_attach_from_config'
321 # Return the 'config' encoded within a normal XMLRPC response so that
322 # we can use the regular response/error parsing code.
323 config = xmlrpc.client.dumps(tuple([resp]), "vdi_attach_from_config")
324 return xmlrpc.client.dumps((config, ), "", True)
326 def attach_from_config(self, sr_uuid, vdi_uuid):
327 """Used for HA State-file only. Will not just attach the VDI but
328 also start a tapdisk on the file"""
329 util.SMlog("SMBFileVDI.attach_from_config")
330 try:
331 if not util.pathexists(self.sr.path):
332 self.sr.attach(sr_uuid)
333 except:
334 util.logException("SMBFileVDI.attach_from_config")
335 raise xs_errors.XenError('SRUnavailable', \
336 opterr='Unable to attach from config')
339if __name__ == '__main__': 339 ↛ 340line 339 didn't jump to line 340, because the condition on line 339 was never true
340 SRCommand.run(SMBSR, DRIVER_INFO)
341else:
342 SR.registerSR(SMBSR)
343#