root / lib / storage / gluster.py @ 178ad717
History | View | Annotate | Download (13.1 kB)
1 |
#
|
---|---|
2 |
#
|
3 |
|
4 |
# Copyright (C) 2013 Google Inc.
|
5 |
#
|
6 |
# This program is free software; you can redistribute it and/or modify
|
7 |
# it under the terms of the GNU General Public License as published by
|
8 |
# the Free Software Foundation; either version 2 of the License, or
|
9 |
# (at your option) any later version.
|
10 |
#
|
11 |
# This program is distributed in the hope that it will be useful, but
|
12 |
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
14 |
# General Public License for more details.
|
15 |
#
|
16 |
# You should have received a copy of the GNU General Public License
|
17 |
# along with this program; if not, write to the Free Software
|
18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
19 |
# 02110-1301, USA.
|
20 |
|
21 |
"""Gluster storage class.
|
22 |
|
23 |
This class is very similar to FileStorage, given that Gluster when mounted
|
24 |
behaves essentially like a regular file system. Unlike RBD, there are no
|
25 |
special provisions for block device abstractions (yet).
|
26 |
|
27 |
"""
|
28 |
import logging |
29 |
import os |
30 |
import socket |
31 |
|
32 |
from ganeti import utils |
33 |
from ganeti import errors |
34 |
from ganeti import netutils |
35 |
from ganeti import constants |
36 |
from ganeti import ssconf |
37 |
|
38 |
from ganeti.utils import io |
39 |
from ganeti.storage import base |
40 |
from ganeti.storage.filestorage import FileDeviceHelper |
41 |
|
42 |
|
43 |
class GlusterVolume(object): |
44 |
"""This class represents a Gluster volume.
|
45 |
|
46 |
Volumes are uniquely identified by:
|
47 |
|
48 |
- their IP address
|
49 |
- their port
|
50 |
- the volume name itself
|
51 |
|
52 |
Two GlusterVolume objects x, y with same IP address, port and volume name
|
53 |
are considered equal.
|
54 |
|
55 |
"""
|
56 |
|
57 |
def __init__(self, server_addr, port, volume, _run_cmd=utils.RunCmd, |
58 |
_mount_point=None):
|
59 |
"""Creates a Gluster volume object.
|
60 |
|
61 |
@type server_addr: str
|
62 |
@param server_addr: The address to connect to
|
63 |
|
64 |
@type port: int
|
65 |
@param port: The port to connect to (Gluster standard is 24007)
|
66 |
|
67 |
@type volume: str
|
68 |
@param volume: The gluster volume to use for storage.
|
69 |
|
70 |
"""
|
71 |
self.server_addr = server_addr
|
72 |
server_ip = netutils.Hostname.GetIP(self.server_addr)
|
73 |
self._server_ip = server_ip
|
74 |
port = netutils.ValidatePortNumber(port) |
75 |
self._port = port
|
76 |
self._volume = volume
|
77 |
if _mount_point: # tests |
78 |
self.mount_point = _mount_point
|
79 |
else:
|
80 |
self.mount_point = ssconf.SimpleStore().GetGlusterStorageDir()
|
81 |
|
82 |
self._run_cmd = _run_cmd
|
83 |
|
84 |
@property
|
85 |
def server_ip(self): |
86 |
return self._server_ip |
87 |
|
88 |
@property
|
89 |
def port(self): |
90 |
return self._port |
91 |
|
92 |
@property
|
93 |
def volume(self): |
94 |
return self._volume |
95 |
|
96 |
def __eq__(self, other): |
97 |
return (self.server_ip, self.port, self.volume) == \ |
98 |
(other.server_ip, other.port, other.volume) |
99 |
|
100 |
def __repr__(self): |
101 |
return """GlusterVolume("{ip}", {port}, "{volume}")""" \ |
102 |
.format(ip=self.server_ip, port=self.port, volume=self.volume) |
103 |
|
104 |
def __hash__(self): |
105 |
return (self.server_ip, self.port, self.volume).__hash__() |
106 |
|
107 |
def _IsMounted(self): |
108 |
"""Checks if we are mounted or not.
|
109 |
|
110 |
@rtype: bool
|
111 |
@return: True if this volume is mounted.
|
112 |
|
113 |
"""
|
114 |
if not os.path.exists(self.mount_point): |
115 |
return False |
116 |
|
117 |
return os.path.ismount(self.mount_point) |
118 |
|
119 |
def _GuessMountFailReasons(self): |
120 |
"""Try and give reasons why the mount might've failed.
|
121 |
|
122 |
@rtype: str
|
123 |
@return: A semicolon-separated list of problems found with the current setup
|
124 |
suitable for display to the user.
|
125 |
|
126 |
"""
|
127 |
|
128 |
reasons = [] |
129 |
|
130 |
# Does the mount point exist?
|
131 |
if not os.path.exists(self.mount_point): |
132 |
reasons.append("%r: does not exist" % self.mount_point) |
133 |
|
134 |
# Okay, it exists, but is it a directory?
|
135 |
elif not os.path.isdir(self.mount_point): |
136 |
reasons.append("%r: not a directory" % self.mount_point) |
137 |
|
138 |
# If, for some unfortunate reason, this folder exists before mounting:
|
139 |
#
|
140 |
# /var/run/ganeti/gluster/gv0/10.0.0.1:30000:gv0/
|
141 |
# '--------- cwd ------------'
|
142 |
#
|
143 |
# and you _are_ trying to mount the gluster volume gv0 on 10.0.0.1:30000,
|
144 |
# then the mount.glusterfs command parser gets confused and this command:
|
145 |
#
|
146 |
# mount -t glusterfs 10.0.0.1:30000:gv0 /var/run/ganeti/gluster/gv0
|
147 |
# '-- remote end --' '------ mountpoint -------'
|
148 |
#
|
149 |
# gets parsed instead like this:
|
150 |
#
|
151 |
# mount -t glusterfs 10.0.0.1:30000:gv0 /var/run/ganeti/gluster/gv0
|
152 |
# '-- mountpoint --' '----- syntax error ------'
|
153 |
#
|
154 |
# and if there _is_ a gluster server running locally at the default remote
|
155 |
# end, localhost:24007, then this is not a network error and therefore... no
|
156 |
# usage message gets printed out. All you get is a Byson parser error in the
|
157 |
# gluster log files about an unexpected token in line 1, "". (That's stdin.)
|
158 |
#
|
159 |
# Not that we rely on that output in any way whatsoever...
|
160 |
|
161 |
parser_confusing = io.PathJoin(self.mount_point,
|
162 |
self._GetFUSEMountString())
|
163 |
if os.path.exists(parser_confusing):
|
164 |
reasons.append("%r: please delete, rename or move." % parser_confusing)
|
165 |
|
166 |
# Let's try something else: can we connect to the server?
|
167 |
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
168 |
try:
|
169 |
sock.connect((self.server_ip, self.port)) |
170 |
sock.close() |
171 |
except socket.error as err: |
172 |
reasons.append("%s:%d: %s" % (self.server_ip, self.port, err.strerror)) |
173 |
|
174 |
reasons.append("try running 'gluster volume info %s' on %s to ensure"
|
175 |
" it exists, it is started and it is using the tcp"
|
176 |
" transport" % (self.volume, self.server_ip)) |
177 |
|
178 |
return "; ".join(reasons) |
179 |
|
180 |
def _GetFUSEMountString(self): |
181 |
"""Return the string FUSE needs to mount this volume.
|
182 |
|
183 |
@rtype: str
|
184 |
"""
|
185 |
|
186 |
return "{ip}:{port}:{volume}" \ |
187 |
.format(ip=self.server_ip, port=self.port, volume=self.volume) |
188 |
|
189 |
def GetKVMMountString(self, path): |
190 |
"""Return the string KVM needs to use this volume.
|
191 |
|
192 |
@rtype: str
|
193 |
"""
|
194 |
|
195 |
ip = self.server_ip
|
196 |
if netutils.IPAddress.GetAddressFamily(ip) == socket.AF_INET6:
|
197 |
ip = "[%s]" % ip
|
198 |
return "gluster://{ip}:{port}/{volume}/{path}" \ |
199 |
.format(ip=ip, port=self.port, volume=self.volume, path=path) |
200 |
|
201 |
def Mount(self): |
202 |
"""Try and mount the volume. No-op if the volume is already mounted.
|
203 |
|
204 |
@raises BlockDeviceError: if the mount was unsuccessful
|
205 |
|
206 |
@rtype: context manager
|
207 |
@return: A simple context manager that lets you use this volume for
|
208 |
short lived operations like so::
|
209 |
|
210 |
with volume.mount():
|
211 |
# Do operations on volume
|
212 |
# Volume is now unmounted
|
213 |
|
214 |
"""
|
215 |
|
216 |
class _GlusterVolumeContextManager(object): |
217 |
|
218 |
def __init__(self, volume): |
219 |
self.volume = volume
|
220 |
|
221 |
def __enter__(self): |
222 |
# We're already mounted.
|
223 |
return self |
224 |
|
225 |
def __exit__(self, *exception_information): |
226 |
self.volume.Unmount()
|
227 |
return False # do not swallow exceptions. |
228 |
|
229 |
if self._IsMounted(): |
230 |
return _GlusterVolumeContextManager(self) |
231 |
|
232 |
command = ["mount",
|
233 |
"-t", "glusterfs", |
234 |
self._GetFUSEMountString(),
|
235 |
self.mount_point]
|
236 |
|
237 |
io.Makedirs(self.mount_point)
|
238 |
self._run_cmd(" ".join(command), |
239 |
# Why set cwd? Because it's an area we control. If,
|
240 |
# for some unfortunate reason, this folder exists:
|
241 |
# "/%s/" % _GetFUSEMountString()
|
242 |
# ...then the gluster parser gets confused and treats
|
243 |
# _GetFUSEMountString() as your mount point and
|
244 |
# self.mount_point becomes a syntax error.
|
245 |
cwd=self.mount_point)
|
246 |
|
247 |
# mount.glusterfs exits with code 0 even after failure.
|
248 |
# https://bugzilla.redhat.com/show_bug.cgi?id=1031973
|
249 |
if not self._IsMounted(): |
250 |
reasons = self._GuessMountFailReasons()
|
251 |
if not reasons: |
252 |
reasons = "%r failed." % (" ".join(command)) |
253 |
base.ThrowError("%r: mount failure: %s",
|
254 |
self.mount_point,
|
255 |
reasons) |
256 |
|
257 |
return _GlusterVolumeContextManager(self) |
258 |
|
259 |
def Unmount(self): |
260 |
"""Try and unmount the volume.
|
261 |
|
262 |
Failures are logged but otherwise ignored.
|
263 |
|
264 |
@raises BlockDeviceError: if the volume was not mounted to begin with.
|
265 |
"""
|
266 |
|
267 |
if not self._IsMounted(): |
268 |
base.ThrowError("%r: should be mounted but isn't.", self.mount_point) |
269 |
|
270 |
result = self._run_cmd(["umount", |
271 |
self.mount_point])
|
272 |
|
273 |
if result.failed:
|
274 |
logging.warning("Failed to unmount %r from %r: %s",
|
275 |
self, self.mount_point, result.fail_reason) |
276 |
|
277 |
|
278 |
class GlusterStorage(base.BlockDev): |
279 |
"""File device using the Gluster backend.
|
280 |
|
281 |
This class represents a file storage backend device stored on Gluster. Ganeti
|
282 |
mounts and unmounts the Gluster devices automatically.
|
283 |
|
284 |
The unique_id for the file device is a (file_driver, file_path) tuple.
|
285 |
|
286 |
"""
|
287 |
def __init__(self, unique_id, children, size, params, dyn_params): |
288 |
"""Initalizes a file device backend.
|
289 |
|
290 |
"""
|
291 |
if children:
|
292 |
base.ThrowError("Invalid setup for file device")
|
293 |
|
294 |
try:
|
295 |
driver, path = unique_id |
296 |
except ValueError: # wrong number of arguments |
297 |
raise ValueError("Invalid configuration data %s" % repr(unique_id)) |
298 |
|
299 |
server_addr = params[constants.GLUSTER_HOST] |
300 |
port = params[constants.GLUSTER_PORT] |
301 |
volume = params[constants.GLUSTER_VOLUME] |
302 |
|
303 |
self.volume = GlusterVolume(server_addr, port, volume)
|
304 |
self.path = path
|
305 |
self.driver = driver
|
306 |
self.full_path = io.PathJoin(self.volume.mount_point, self.path) |
307 |
self.file = None |
308 |
|
309 |
super(GlusterStorage, self).__init__(unique_id, children, size, |
310 |
params, dyn_params) |
311 |
|
312 |
self.Attach()
|
313 |
|
314 |
def Assemble(self): |
315 |
"""Assemble the device.
|
316 |
|
317 |
Checks whether the file device exists, raises BlockDeviceError otherwise.
|
318 |
|
319 |
"""
|
320 |
assert self.attached, "Gluster file assembled without being attached" |
321 |
self.file.Exists(assert_exists=True) |
322 |
|
323 |
def Shutdown(self): |
324 |
"""Shutdown the device.
|
325 |
|
326 |
"""
|
327 |
|
328 |
self.file = None |
329 |
self.dev_path = None |
330 |
self.attached = False |
331 |
|
332 |
def Open(self, force=False): |
333 |
"""Make the device ready for I/O.
|
334 |
|
335 |
This is a no-op for the file type.
|
336 |
|
337 |
"""
|
338 |
assert self.attached, "Gluster file opened without being attached" |
339 |
|
340 |
def Close(self): |
341 |
"""Notifies that the device will no longer be used for I/O.
|
342 |
|
343 |
This is a no-op for the file type.
|
344 |
"""
|
345 |
pass
|
346 |
|
347 |
def Remove(self): |
348 |
"""Remove the file backing the block device.
|
349 |
|
350 |
@rtype: boolean
|
351 |
@return: True if the removal was successful
|
352 |
|
353 |
"""
|
354 |
with self.volume.Mount(): |
355 |
self.file = FileDeviceHelper(self.full_path) |
356 |
if self.file.Remove(): |
357 |
self.file = None |
358 |
return True |
359 |
else:
|
360 |
return False |
361 |
|
362 |
def Rename(self, new_id): |
363 |
"""Renames the file.
|
364 |
|
365 |
"""
|
366 |
# TODO: implement rename for file-based storage
|
367 |
base.ThrowError("Rename is not supported for Gluster storage")
|
368 |
|
369 |
def Grow(self, amount, dryrun, backingstore, excl_stor): |
370 |
"""Grow the file
|
371 |
|
372 |
@param amount: the amount (in mebibytes) to grow with
|
373 |
|
374 |
"""
|
375 |
self.file.Grow(amount, dryrun, backingstore, excl_stor)
|
376 |
|
377 |
def Attach(self): |
378 |
"""Attach to an existing file.
|
379 |
|
380 |
Check if this file already exists.
|
381 |
|
382 |
@rtype: boolean
|
383 |
@return: True if file exists
|
384 |
|
385 |
"""
|
386 |
try:
|
387 |
self.volume.Mount()
|
388 |
self.file = FileDeviceHelper(self.full_path) |
389 |
self.dev_path = self.full_path |
390 |
except Exception as err: |
391 |
self.volume.Unmount()
|
392 |
raise err
|
393 |
|
394 |
self.attached = self.file.Exists() |
395 |
return self.attached |
396 |
|
397 |
def GetActualSize(self): |
398 |
"""Return the actual disk size.
|
399 |
|
400 |
@note: the device needs to be active when this is called
|
401 |
|
402 |
"""
|
403 |
return self.file.Size() |
404 |
|
405 |
def GetUserspaceAccessUri(self, hypervisor): |
406 |
"""Generate KVM userspace URIs to be used as `-drive file` settings.
|
407 |
|
408 |
@see: L{BlockDev.GetUserspaceAccessUri}
|
409 |
@see: https://github.com/qemu/qemu/commit/8d6d89cb63c57569864ecdeb84d3a1c2eb
|
410 |
"""
|
411 |
|
412 |
if hypervisor == constants.HT_KVM:
|
413 |
return self.volume.GetKVMMountString(self.path) |
414 |
else:
|
415 |
base.ThrowError("Hypervisor %s doesn't support Gluster userspace access" %
|
416 |
hypervisor) |
417 |
|
418 |
@classmethod
|
419 |
def Create(cls, unique_id, children, size, spindles, params, excl_stor, |
420 |
dyn_params): |
421 |
"""Create a new file.
|
422 |
|
423 |
@param size: the size of file in MiB
|
424 |
|
425 |
@rtype: L{bdev.FileStorage}
|
426 |
@return: an instance of FileStorage
|
427 |
|
428 |
"""
|
429 |
if excl_stor:
|
430 |
raise errors.ProgrammerError("FileStorage device requested with" |
431 |
" exclusive_storage")
|
432 |
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: |
433 |
raise ValueError("Invalid configuration data %s" % str(unique_id)) |
434 |
|
435 |
full_path = unique_id[1]
|
436 |
|
437 |
server_addr = params[constants.GLUSTER_HOST] |
438 |
port = params[constants.GLUSTER_PORT] |
439 |
volume = params[constants.GLUSTER_VOLUME] |
440 |
|
441 |
volume_obj = GlusterVolume(server_addr, port, volume) |
442 |
full_path = io.PathJoin(volume_obj.mount_point, full_path) |
443 |
|
444 |
# Possible optimization: defer actual creation to first Attach, rather
|
445 |
# than mounting and unmounting here, then remounting immediately after.
|
446 |
with volume_obj.Mount():
|
447 |
FileDeviceHelper.CreateFile(full_path, size, create_folders=True)
|
448 |
|
449 |
return GlusterStorage(unique_id, children, size, params, dyn_params)
|