Coverage for drivers/NFSSR.py : 57%

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# FileSR: local-file storage repository
20import socket
22import SR
23import SRCommand
24import FileSR
25import util
26import errno
27import os
28import sys
29import xmlrpc.client
30import xs_errors
31import nfs
32import vhdutil
33from lock import Lock
34import cleanup
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",
39 "VDI_GENERATE_CONFIG", "VDI_MIRROR",
40 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE", "VDI_CONFIG_CBT",
41 "VDI_ACTIVATE", "VDI_DEACTIVATE", "THIN_PROVISIONING", "VDI_READ_CACHING"]
43CONFIGURATION = [['server', 'hostname or IP address of NFS server (required)'],
44 ['serverpath', 'path on remote server (required)'],
45 nfs.NFS_VERSION]
47DRIVER_INFO = {
48 'name': 'NFS VHD',
49 'description': 'SR plugin which stores disks as VHD files on a remote NFS filesystem',
50 'vendor': 'Citrix Systems Inc',
51 'copyright': '(C) 2008 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 = "probe"
63NFSPORT = 2049
64DEFAULT_TRANSPORT = "tcp"
65PROBEVERSION = 'probeversion'
68class NFSSR(FileSR.SharedFileSR):
69 """NFS file-based storage repository"""
71 def handles(type):
72 return type == 'nfs'
73 handles = staticmethod(handles)
75 def load(self, sr_uuid):
76 self.ops_exclusive = FileSR.OPS_EXCLUSIVE
77 self.lock = Lock(vhdutil.LOCK_TYPE_SR, self.uuid)
78 self.sr_vditype = SR.DEFAULT_TAP
79 self.driver_config = DRIVER_CONFIG
80 if 'server' not in self.dconf: 80 ↛ 81line 80 didn't jump to line 81, because the condition on line 80 was never true
81 raise xs_errors.XenError('ConfigServerMissing')
82 self.remoteserver = self.dconf['server']
83 self.nosubdir = False
84 if self.sr_ref and self.session is not None: 84 ↛ 85line 84 didn't jump to line 85, because the condition on line 84 was never true
85 self.sm_config = self.session.xenapi.SR.get_sm_config(self.sr_ref)
86 self.other_config = self.session.xenapi.SR.get_other_config(self.sr_ref)
87 else:
88 self.sm_config = self.srcmd.params.get('sr_sm_config') or {}
89 self.other_config = self.srcmd.params.get('sr_other_config') or {}
90 self.nosubdir = self.sm_config.get('nosubdir') == "true"
91 serverpath = self.dconf.get('serverpath')
92 if serverpath is not None: 92 ↛ 97line 92 didn't jump to line 97, because the condition on line 92 was never false
93 self.remotepath = os.path.join(
94 serverpath,
95 not self.nosubdir and sr_uuid or ""
96 )
97 self.path = os.path.join(SR.MOUNT_BASE, sr_uuid)
99 # Handle optional dconf attributes
100 self.set_transport()
101 self.nfsversion = nfs.validate_nfsversion(self.dconf.get('nfsversion'))
102 if 'options' in self.dconf:
103 self.options = self.dconf['options']
104 else:
105 self.options = ''
107 def validate_remotepath(self, scan):
108 serverpath = self.dconf.get('serverpath')
109 if serverpath is None: 109 ↛ 110line 109 didn't jump to line 110, because the condition on line 109 was never true
110 if scan:
111 try:
112 self.scan_exports(self.dconf['server'])
113 except:
114 pass
115 raise xs_errors.XenError('ConfigServerPathMissing')
117 def check_server(self):
118 try:
119 if PROBEVERSION in self.dconf: 119 ↛ 120line 119 didn't jump to line 120, because the condition on line 119 was never true
120 sv = nfs.get_supported_nfs_versions(self.remoteserver, self.transport)
121 if len(sv):
122 self.nfsversion = sv[0]
123 else:
124 if not nfs.check_server_tcp(self.remoteserver, self.transport, self.nfsversion): 124 ↛ 125line 124 didn't jump to line 125, because the condition on line 124 was never true
125 raise nfs.NfsException("Unsupported NFS version: %s" % self.nfsversion)
127 except nfs.NfsException as exc:
128 raise xs_errors.XenError('NFSVersion',
129 opterr=exc.errstr)
131 def mount(self, mountpoint, remotepath, timeout=None, retrans=None):
132 try:
133 nfs.soft_mount(
134 mountpoint, self.remoteserver, remotepath, self.transport,
135 useroptions=self.options, timeout=timeout,
136 nfsversion=self.nfsversion, retrans=retrans)
137 except nfs.NfsException as exc:
138 raise xs_errors.XenError('NFSMount', opterr=exc.errstr)
140 def attach(self, sr_uuid):
141 if not self._checkmount():
142 try:
143 self.validate_remotepath(False)
144 util._testHost(self.dconf['server'], NFSPORT, 'NFSTarget')
145 self.mount_remotepath(sr_uuid)
146 self._check_writable()
147 self._check_hardlinks()
148 except:
149 if self._checkmount():
150 nfs.unmount(self.path, True)
151 raise
152 self.attached = True
154 def mount_remotepath(self, sr_uuid):
155 if not self._checkmount(): 155 ↛ exitline 155 didn't return from function 'mount_remotepath', because the condition on line 155 was never false
156 # FIXME: What is the purpose of this check_server?
157 # It doesn't stop us from continuing if the server
158 # doesn't support the requested version. We fail
159 # in mount instead
160 self.check_server()
161 # Extract timeout and retrans values, if any
162 io_timeout = nfs.get_nfs_timeout(self.other_config)
163 io_retrans = nfs.get_nfs_retrans(self.other_config)
164 self.mount(self.path, self.remotepath,
165 timeout=io_timeout, retrans=io_retrans)
167 def probe(self):
168 # Verify NFS target and port
169 util._testHost(self.dconf['server'], NFSPORT, 'NFSTarget')
171 self.validate_remotepath(True)
172 self.check_server()
174 temppath = os.path.join(SR.MOUNT_BASE, PROBE_MOUNTPOINT)
176 self.mount(temppath, self.remotepath)
177 try:
178 return nfs.scan_srlist(temppath, self.transport, self.dconf)
179 finally:
180 try:
181 nfs.unmount(temppath, True)
182 except:
183 pass
185 def detach(self, sr_uuid):
186 """Detach the SR: Unmounts and removes the mountpoint"""
187 if not self._checkmount(): 187 ↛ 189line 187 didn't jump to line 189, because the condition on line 187 was never false
188 return
189 util.SMlog("Aborting GC/coalesce")
190 cleanup.abort(self.uuid)
192 # Change directory to avoid unmount conflicts
193 os.chdir(SR.MOUNT_BASE)
195 try:
196 nfs.unmount(self.path, True)
197 except nfs.NfsException as exc:
198 raise xs_errors.XenError('NFSUnMount', opterr=exc.errstr)
200 self.attached = False
202 def create(self, sr_uuid, size):
203 util._testHost(self.dconf['server'], NFSPORT, 'NFSTarget')
204 self.validate_remotepath(True)
205 if self._checkmount(): 205 ↛ 206line 205 didn't jump to line 206, because the condition on line 205 was never true
206 raise xs_errors.XenError('NFSAttached')
208 # Set the target path temporarily to the base dir
209 # so that we can create the target SR directory
210 self.remotepath = self.dconf['serverpath']
211 try:
212 self.mount_remotepath(sr_uuid)
213 except Exception as exn:
214 try:
215 os.rmdir(self.path)
216 except:
217 pass
218 raise
220 if not self.nosubdir: 220 ↛ 240line 220 didn't jump to line 240, because the condition on line 220 was never false
221 newpath = os.path.join(self.path, sr_uuid)
222 if util.ioretry(lambda: util.pathexists(newpath)): 222 ↛ 223line 222 didn't jump to line 223, because the condition on line 222 was never true
223 if len(util.ioretry(lambda: util.listdir(newpath))) != 0:
224 self.detach(sr_uuid)
225 raise xs_errors.XenError('SRExists')
226 else:
227 try:
228 util.ioretry(lambda: util.makedirs(newpath))
229 except util.CommandException as inst:
230 if inst.code != errno.EEXIST: 230 ↛ 240line 230 didn't jump to line 240, because the condition on line 230 was never false
231 self.detach(sr_uuid)
232 if inst.code == errno.EROFS:
233 raise xs_errors.XenError('SharedFileSystemNoWrite',
234 opterr='remote filesystem is read-only error is %d'
235 % inst.code)
236 else:
237 raise xs_errors.XenError('NFSCreate',
238 opterr='remote directory creation error is %d'
239 % inst.code)
240 self.detach(sr_uuid)
242 def delete(self, sr_uuid):
243 # try to remove/delete non VDI contents first
244 super(NFSSR, self).delete(sr_uuid)
245 try:
246 if self._checkmount():
247 self.detach(sr_uuid)
249 # Set the target path temporarily to the base dir
250 # so that we can remove the target SR directory
251 self.remotepath = self.dconf['serverpath']
252 self.mount_remotepath(sr_uuid)
253 if not self.nosubdir:
254 newpath = os.path.join(self.path, sr_uuid)
255 if util.ioretry(lambda: util.pathexists(newpath)):
256 util.ioretry(lambda: os.rmdir(newpath))
257 self.detach(sr_uuid)
258 except util.CommandException as inst:
259 self.detach(sr_uuid)
260 if inst.code != errno.ENOENT:
261 raise xs_errors.XenError('NFSDelete')
263 def vdi(self, uuid):
264 return NFSFileVDI(self, uuid)
266 def scan_exports(self, target):
267 util.SMlog("scanning2 (target=%s)" % target)
268 dom = nfs.scan_exports(target, self.transport)
269 print(dom.toprettyxml(), file=sys.stderr)
271 def set_transport(self):
272 self.transport = DEFAULT_TRANSPORT
273 if self.remoteserver is None:
274 # CA-365359: on_slave.is_open sends a dconf with {"server": None}
275 return
277 try:
278 addr_info = socket.getaddrinfo(self.remoteserver, 0)[0]
279 except Exception:
280 return
282 use_ipv6 = addr_info[0] == socket.AF_INET6
283 if use_ipv6: 283 ↛ 285line 283 didn't jump to line 285, because the condition on line 283 was never false
284 self.transport = 'tcp6'
285 if 'useUDP' in self.dconf and self.dconf['useUDP'] == 'true': 285 ↛ 286line 285 didn't jump to line 286, because the condition on line 285 was never true
286 self.transport = 'udp6' if use_ipv6 else 'udp'
289class NFSFileVDI(FileSR.FileVDI):
290 def attach(self, sr_uuid, vdi_uuid):
291 if not hasattr(self, 'xenstore_data'):
292 self.xenstore_data = {}
294 self.xenstore_data["storage-type"] = "nfs"
296 return super(NFSFileVDI, self).attach(sr_uuid, vdi_uuid)
298 def generate_config(self, sr_uuid, vdi_uuid):
299 util.SMlog("NFSFileVDI.generate_config")
300 if not util.pathexists(self.path):
301 raise xs_errors.XenError('VDIUnavailable')
302 resp = {}
303 resp['device_config'] = self.sr.dconf
304 resp['sr_uuid'] = sr_uuid
305 resp['vdi_uuid'] = vdi_uuid
306 resp['sr_sm_config'] = self.sr.sm_config
307 resp['sr_other_config'] = self.sr.other_config
308 resp['command'] = 'vdi_attach_from_config'
309 # Return the 'config' encoded within a normal XMLRPC response so that
310 # we can use the regular response/error parsing code.
311 config = xmlrpc.client.dumps(tuple([resp]), "vdi_attach_from_config")
312 return xmlrpc.client.dumps((config, ), "", True)
314 def attach_from_config(self, sr_uuid, vdi_uuid):
315 """Used for HA State-file only. Will not just attach the VDI but
316 also start a tapdisk on the file"""
317 util.SMlog("NFSFileVDI.attach_from_config")
318 try:
319 self.sr.attach(sr_uuid)
320 except:
321 util.logException("NFSFileVDI.attach_from_config")
322 raise xs_errors.XenError('SRUnavailable', \
323 opterr='Unable to attach from config')
326if __name__ == '__main__': 326 ↛ 327line 326 didn't jump to line 327, because the condition on line 326 was never true
327 SRCommand.run(NFSSR, DRIVER_INFO)
328else:
329 SR.registerSR(NFSSR)