Coverage for drivers/SMBSR.py : 43%

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