Coverage for drivers/NFSSR.py : 59%

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