Coverage for libs/sm/drivers/NFSSR.py: 56%
204 statements
« prev ^ index » next coverage.py v7.3.2, created at 2026-07-10 07:59 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2026-07-10 07:59 +0000
1# Copyright (C) Citrix Systems Inc.
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU Lesser General Public License as published
5# by the Free Software Foundation; version 2.1 only.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU Lesser General Public License for more details.
11#
12# You should have received a copy of the GNU Lesser General Public License
13# along with this program; if not, write to the Free Software Foundation, Inc.,
14# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15#
16# NFSSR: NFS-based file storage repository
17# matches with drivers/NFSSR
19import socket
20import errno
21import os
22import sys
23import xmlrpc.client
25from sm import SR
26from sm import nfs
27from sm import vhdutil
28from sm import cleanup
29from sm.drivers import FileSR
30from sm.core import util
31from sm.core import xs_errors
32from sm.core.lock import Lock
34CAPABILITIES = ["SR_PROBE", "SR_UPDATE", "SR_CACHING",
35 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH",
36 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE",
37 "VDI_GENERATE_CONFIG", "VDI_MIRROR",
38 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE", "VDI_CONFIG_CBT",
39 "VDI_ACTIVATE", "VDI_DEACTIVATE", "THIN_PROVISIONING", "VDI_READ_CACHING"]
41CONFIGURATION = [['server', 'hostname or IP address of NFS server (required)'],
42 ['serverpath', 'path on remote server (required)'],
43 nfs.NFS_VERSION]
45DRIVER_INFO = {
46 'name': 'NFS VHD',
47 'description': 'SR plugin which stores disks as VHD files on a remote NFS filesystem',
48 'vendor': 'Citrix Systems Inc',
49 'copyright': '(C) 2008 Citrix Systems Inc',
50 'driver_version': '1.0',
51 'required_api_version': '1.0',
52 'capabilities': CAPABILITIES,
53 'configuration': CONFIGURATION
54 }
56DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True}
58# The mountpoint for the directory when performing an sr_probe. All probes
59# are guaranteed to be serialised by xapi, so this single mountpoint is fine.
60PROBE_MOUNTPOINT = "probe"
61NFSPORT = 2049
62DEFAULT_TRANSPORT = "tcp"
63PROBEVERSION = 'probeversion'
66class NFSSR(FileSR.SharedFileSR):
67 """NFS file-based storage repository"""
69 def handles(type):
70 return type == 'nfs'
71 handles = staticmethod(handles)
73 def load(self, sr_uuid):
74 self.ops_exclusive = FileSR.OPS_EXCLUSIVE
75 self.lock = Lock(vhdutil.LOCK_TYPE_SR, self.uuid)
76 self.sr_vditype = SR.DEFAULT_TAP
77 self.driver_config = DRIVER_CONFIG
78 if 'server' not in self.dconf: 78 ↛ 79line 78 didn't jump to line 79, because the condition on line 78 was never true
79 raise xs_errors.XenError('ConfigServerMissing')
80 self.remoteserver = self.dconf['server']
81 self.nosubdir = False
82 if self.sr_ref and self.session is not None: 82 ↛ 83line 82 didn't jump to line 83, because the condition on line 82 was never true
83 self.sm_config = self.session.xenapi.SR.get_sm_config(self.sr_ref)
84 self.other_config = self.session.xenapi.SR.get_other_config(self.sr_ref)
85 else:
86 self.sm_config = self.srcmd.params.get('sr_sm_config') or {}
87 self.other_config = self.srcmd.params.get('sr_other_config') or {}
88 self.nosubdir = self.sm_config.get('nosubdir') == "true"
89 if 'serverpath' in self.dconf: 89 ↛ 92line 89 didn't jump to line 92, because the condition on line 89 was never false
90 self.remotepath = os.path.join(self.dconf['serverpath'],
91 not self.nosubdir and sr_uuid or "")
92 self.path = os.path.join(SR.MOUNT_BASE, sr_uuid)
94 # Handle optional dconf attributes
95 self.set_transport()
96 self.nfsversion = nfs.validate_nfsversion(self.dconf.get('nfsversion'))
97 if 'options' in self.dconf:
98 self.options = self.dconf['options']
99 else:
100 self.options = ''
102 def validate_remotepath(self, scan):
103 if 'serverpath' not in self.dconf: 103 ↛ 104line 103 didn't jump to line 104, because the condition on line 103 was never true
104 if scan:
105 try:
106 self.scan_exports(self.dconf['server'])
107 except:
108 pass
109 raise xs_errors.XenError('ConfigServerPathMissing')
111 def check_server(self):
112 try:
113 if PROBEVERSION in self.dconf: 113 ↛ 114line 113 didn't jump to line 114, because the condition on line 113 was never true
114 sv = nfs.get_supported_nfs_versions(self.remoteserver, self.transport)
115 if len(sv):
116 self.nfsversion = sv[0]
117 else:
118 if not nfs.check_server_tcp(self.remoteserver, self.transport, self.nfsversion): 118 ↛ 119line 118 didn't jump to line 119, because the condition on line 118 was never true
119 raise nfs.NfsException("Unsupported NFS version: %s" % self.nfsversion)
121 except nfs.NfsException as exc:
122 raise xs_errors.XenError('NFSVersion',
123 opterr=exc.errstr)
125 def mount(self, mountpoint, remotepath, timeout=None, retrans=None):
126 try:
127 nfs.soft_mount(
128 mountpoint, self.remoteserver, remotepath, self.transport,
129 useroptions=self.options, timeout=timeout,
130 nfsversion=self.nfsversion, retrans=retrans)
131 except nfs.NfsException as exc:
132 raise xs_errors.XenError('NFSMount', opterr=exc.errstr)
134 def attach(self, sr_uuid):
135 if not self._checkmount():
136 try:
137 self.validate_remotepath(False)
138 util._testHost(self.dconf['server'], NFSPORT, 'NFSTarget')
139 self.mount_remotepath(sr_uuid)
140 self._check_writable()
141 self._check_hardlinks()
142 except:
143 if self._checkmount():
144 nfs.unmount(self.path, True)
145 raise
146 self.attached = True
148 def mount_remotepath(self, sr_uuid):
149 if not self._checkmount(): 149 ↛ exitline 149 didn't return from function 'mount_remotepath', because the condition on line 149 was never false
150 # FIXME: What is the purpose of this check_server?
151 # It doesn't stop us from continuing if the server
152 # doesn't support the requested version. We fail
153 # in mount instead
154 self.check_server()
155 # Extract timeout and retrans values, if any
156 io_timeout = nfs.get_nfs_timeout(self.other_config)
157 io_retrans = nfs.get_nfs_retrans(self.other_config)
158 self.mount(self.path, self.remotepath,
159 timeout=io_timeout, retrans=io_retrans)
161 def probe(self):
162 # Verify NFS target and port
163 util._testHost(self.dconf['server'], NFSPORT, 'NFSTarget')
165 self.validate_remotepath(True)
166 self.check_server()
168 temppath = os.path.join(SR.MOUNT_BASE, PROBE_MOUNTPOINT)
170 self.mount(temppath, self.remotepath)
171 try:
172 return nfs.scan_srlist(temppath, self.transport, self.dconf)
173 finally:
174 try:
175 nfs.unmount(temppath, True)
176 except:
177 pass
179 def detach(self, sr_uuid):
180 """Detach the SR: Unmounts and removes the mountpoint"""
181 if not self._checkmount(): 181 ↛ 183line 181 didn't jump to line 183, because the condition on line 181 was never false
182 return
183 util.SMlog("Aborting GC/coalesce")
184 cleanup.abort(self.uuid)
186 # Change directory to avoid unmount conflicts
187 os.chdir(SR.MOUNT_BASE)
189 try:
190 nfs.unmount(self.path, True)
191 except nfs.NfsException as exc:
192 raise xs_errors.XenError('NFSUnMount', opterr=exc.errstr)
194 self.attached = False
196 def create(self, sr_uuid, size):
197 util._testHost(self.dconf['server'], NFSPORT, 'NFSTarget')
198 self.validate_remotepath(True)
199 if self._checkmount(): 199 ↛ 200line 199 didn't jump to line 200, because the condition on line 199 was never true
200 raise xs_errors.XenError('NFSAttached')
202 # Set the target path temporarily to the base dir
203 # so that we can create the target SR directory
204 self.remotepath = self.dconf['serverpath']
205 try:
206 self.mount_remotepath(sr_uuid)
207 except Exception as exn:
208 try:
209 os.rmdir(self.path)
210 except:
211 pass
212 raise
214 if not self.nosubdir: 214 ↛ 234line 214 didn't jump to line 234, because the condition on line 214 was never false
215 newpath = os.path.join(self.path, sr_uuid)
216 if util.ioretry(lambda: util.pathexists(newpath)): 216 ↛ 217line 216 didn't jump to line 217, because the condition on line 216 was never true
217 if len(util.ioretry(lambda: util.listdir(newpath))) != 0:
218 self.detach(sr_uuid)
219 raise xs_errors.XenError('SRExists')
220 else:
221 try:
222 util.ioretry(lambda: util.makedirs(newpath))
223 except util.CommandException as inst:
224 if inst.code != errno.EEXIST: 224 ↛ 234line 224 didn't jump to line 234, because the condition on line 224 was never false
225 self.detach(sr_uuid)
226 if inst.code == errno.EROFS:
227 raise xs_errors.XenError('SharedFileSystemNoWrite',
228 opterr='remote filesystem is read-only error is %d'
229 % inst.code)
230 else:
231 raise xs_errors.XenError('NFSCreate',
232 opterr='remote directory creation error is %d'
233 % inst.code)
234 self.detach(sr_uuid)
236 def delete(self, sr_uuid):
237 # try to remove/delete non VDI contents first
238 super(NFSSR, self).delete(sr_uuid)
239 try:
240 if self._checkmount():
241 self.detach(sr_uuid)
243 # Set the target path temporarily to the base dir
244 # so that we can remove the target SR directory
245 self.remotepath = self.dconf['serverpath']
246 self.mount_remotepath(sr_uuid)
247 if not self.nosubdir:
248 newpath = os.path.join(self.path, sr_uuid)
249 if util.ioretry(lambda: util.pathexists(newpath)):
250 util.ioretry(lambda: os.rmdir(newpath))
251 self.detach(sr_uuid)
252 except util.CommandException as inst:
253 self.detach(sr_uuid)
254 if inst.code != errno.ENOENT:
255 raise xs_errors.XenError('NFSDelete')
257 def vdi(self, uuid):
258 return NFSFileVDI(self, uuid)
260 def scan_exports(self, target):
261 util.SMlog("scanning2 (target=%s)" % target)
262 dom = nfs.scan_exports(target, self.transport)
263 print(dom.toprettyxml(), file=sys.stderr)
265 def set_transport(self):
266 self.transport = DEFAULT_TRANSPORT
267 if self.remoteserver is None:
268 # CA-365359: on_slave.is_open sends a dconf with {"server": None}
269 return
271 try:
272 addr_info = socket.getaddrinfo(self.remoteserver, 0)[0]
273 except Exception:
274 return
276 use_ipv6 = addr_info[0] == socket.AF_INET6
277 if use_ipv6: 277 ↛ 279line 277 didn't jump to line 279, because the condition on line 277 was never false
278 self.transport = 'tcp6'
279 if 'useUDP' in self.dconf and self.dconf['useUDP'] == 'true': 279 ↛ 280line 279 didn't jump to line 280, because the condition on line 279 was never true
280 self.transport = 'udp6' if use_ipv6 else 'udp'
283class NFSFileVDI(FileSR.FileVDI):
284 def attach(self, sr_uuid, vdi_uuid):
285 if not hasattr(self, 'xenstore_data'):
286 self.xenstore_data = {}
288 self.xenstore_data["storage-type"] = "nfs"
290 return super(NFSFileVDI, self).attach(sr_uuid, vdi_uuid)
292 def generate_config(self, sr_uuid, vdi_uuid):
293 util.SMlog("NFSFileVDI.generate_config")
294 if not util.pathexists(self.path):
295 raise xs_errors.XenError('VDIUnavailable')
296 resp = {}
297 resp['device_config'] = self.sr.dconf
298 resp['sr_uuid'] = sr_uuid
299 resp['vdi_uuid'] = vdi_uuid
300 resp['sr_sm_config'] = self.sr.sm_config
301 resp['sr_other_config'] = self.sr.other_config
302 resp['command'] = 'vdi_attach_from_config'
303 # Return the 'config' encoded within a normal XMLRPC response so that
304 # we can use the regular response/error parsing code.
305 config = xmlrpc.client.dumps(tuple([resp]), "vdi_attach_from_config")
306 return xmlrpc.client.dumps((config, ), "", True)
308 def attach_from_config(self, sr_uuid, vdi_uuid):
309 """Used for HA State-file only. Will not just attach the VDI but
310 also start a tapdisk on the file"""
311 util.SMlog("NFSFileVDI.attach_from_config")
312 try:
313 self.sr.attach(sr_uuid)
314 except:
315 util.logException("NFSFileVDI.attach_from_config")
316 raise xs_errors.XenError('SRUnavailable', \
317 opterr='Unable to attach from config')
319# SR registration at import
320SR.registerSR(NFSSR)