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