1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 """Gluster storage class.
31
32 This class is very similar to FileStorage, given that Gluster when mounted
33 behaves essentially like a regular file system. Unlike RBD, there are no
34 special provisions for block device abstractions (yet).
35
36 """
37 import logging
38 import os
39 import socket
40
41 from ganeti import utils
42 from ganeti import errors
43 from ganeti import netutils
44 from ganeti import constants
45 from ganeti import ssconf
46
47 from ganeti.utils import io
48 from ganeti.storage import base
49 from ganeti.storage.filestorage import FileDeviceHelper
53 """This class represents a Gluster volume.
54
55 Volumes are uniquely identified by:
56
57 - their IP address
58 - their port
59 - the volume name itself
60
61 Two GlusterVolume objects x, y with same IP address, port and volume name
62 are considered equal.
63
64 """
65
68 """Creates a Gluster volume object.
69
70 @type server_addr: str
71 @param server_addr: The address to connect to
72
73 @type port: int
74 @param port: The port to connect to (Gluster standard is 24007)
75
76 @type volume: str
77 @param volume: The gluster volume to use for storage.
78
79 """
80 self.server_addr = server_addr
81 server_ip = netutils.Hostname.GetIP(self.server_addr)
82 self._server_ip = server_ip
83 port = netutils.ValidatePortNumber(port)
84 self._port = port
85 self._volume = volume
86 if _mount_point:
87 self.mount_point = _mount_point
88 else:
89 self.mount_point = ssconf.SimpleStore().GetGlusterStorageDir()
90
91 self._run_cmd = _run_cmd
92
93 @property
95 return self._server_ip
96
97 @property
100
101 @property
104
108
112
115
117 """Checks if we are mounted or not.
118
119 @rtype: bool
120 @return: True if this volume is mounted.
121
122 """
123 if not os.path.exists(self.mount_point):
124 return False
125
126 return os.path.ismount(self.mount_point)
127
129 """Try and give reasons why the mount might've failed.
130
131 @rtype: str
132 @return: A semicolon-separated list of problems found with the current setup
133 suitable for display to the user.
134
135 """
136
137 reasons = []
138
139
140 if not os.path.exists(self.mount_point):
141 reasons.append("%r: does not exist" % self.mount_point)
142
143
144 elif not os.path.isdir(self.mount_point):
145 reasons.append("%r: not a directory" % self.mount_point)
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170 parser_confusing = io.PathJoin(self.mount_point,
171 self._GetFUSEMountString())
172 if os.path.exists(parser_confusing):
173 reasons.append("%r: please delete, rename or move." % parser_confusing)
174
175
176 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
177 try:
178 sock.connect((self.server_ip, self.port))
179 sock.close()
180 except socket.error as err:
181 reasons.append("%s:%d: %s" % (self.server_ip, self.port, err.strerror))
182
183 reasons.append("try running 'gluster volume info %s' on %s to ensure"
184 " it exists, it is started and it is using the tcp"
185 " transport" % (self.volume, self.server_ip))
186
187 return "; ".join(reasons)
188
190 """Return the string FUSE needs to mount this volume.
191
192 @rtype: str
193 """
194
195 return "-o server-port={port} {ip}:/{volume}" \
196 .format(port=self.port, ip=self.server_ip, volume=self.volume)
197
199 """Return the string KVM needs to use this volume.
200
201 @rtype: str
202 """
203
204 ip = self.server_ip
205 if netutils.IPAddress.GetAddressFamily(ip) == socket.AF_INET6:
206 ip = "[%s]" % ip
207 return "gluster://{ip}:{port}/{volume}/{path}" \
208 .format(ip=ip, port=self.port, volume=self.volume, path=path)
209
211 """Try and mount the volume. No-op if the volume is already mounted.
212
213 @raises BlockDeviceError: if the mount was unsuccessful
214
215 @rtype: context manager
216 @return: A simple context manager that lets you use this volume for
217 short lived operations like so::
218
219 with volume.mount():
220 # Do operations on volume
221 # Volume is now unmounted
222
223 """
224
225 class _GlusterVolumeContextManager(object):
226
227 def __init__(self, volume):
228 self.volume = volume
229
230 def __enter__(self):
231
232 return self
233
234 def __exit__(self, *exception_information):
235 self.volume.Unmount()
236 return False
237
238 if self._IsMounted():
239 return _GlusterVolumeContextManager(self)
240
241 command = ["mount",
242 "-t", "glusterfs",
243 self._GetFUSEMountString(),
244 self.mount_point]
245
246 io.Makedirs(self.mount_point)
247 self._run_cmd(" ".join(command),
248
249
250
251
252
253
254 cwd=self.mount_point)
255
256
257
258 if not self._IsMounted():
259 reasons = self._GuessMountFailReasons()
260 if not reasons:
261 reasons = "%r failed." % (" ".join(command))
262 base.ThrowError("%r: mount failure: %s",
263 self.mount_point,
264 reasons)
265
266 return _GlusterVolumeContextManager(self)
267
269 """Try and unmount the volume.
270
271 Failures are logged but otherwise ignored.
272
273 @raises BlockDeviceError: if the volume was not mounted to begin with.
274 """
275
276 if not self._IsMounted():
277 base.ThrowError("%r: should be mounted but isn't.", self.mount_point)
278
279 result = self._run_cmd(["umount",
280 self.mount_point])
281
282 if result.failed:
283 logging.warning("Failed to unmount %r from %r: %s",
284 self, self.mount_point, result.fail_reason)
285
288 """File device using the Gluster backend.
289
290 This class represents a file storage backend device stored on Gluster. Ganeti
291 mounts and unmounts the Gluster devices automatically.
292
293 The unique_id for the file device is a (file_driver, file_path) tuple.
294
295 """
296 - def __init__(self, unique_id, children, size, params, dyn_params, *args):
297 """Initalizes a file device backend.
298
299 """
300 if children:
301 base.ThrowError("Invalid setup for file device")
302
303 try:
304 driver, path = unique_id
305 except ValueError:
306 raise ValueError("Invalid configuration data %s" % repr(unique_id))
307
308 server_addr = params[constants.GLUSTER_HOST]
309 port = params[constants.GLUSTER_PORT]
310 volume = params[constants.GLUSTER_VOLUME]
311
312 self.volume = GlusterVolume(server_addr, port, volume)
313 self.path = path
314 self.driver = driver
315 self.full_path = io.PathJoin(self.volume.mount_point, self.path)
316 self.file = None
317
318 super(GlusterStorage, self).__init__(unique_id, children, size,
319 params, dyn_params, *args)
320
321 self.Attach()
322
324 """Assemble the device.
325
326 Checks whether the file device exists, raises BlockDeviceError otherwise.
327
328 """
329 assert self.attached, "Gluster file assembled without being attached"
330 self.file.Exists(assert_exists=True)
331
333 """Shutdown the device.
334
335 """
336
337 self.file = None
338 self.dev_path = None
339 self.attached = False
340
341 - def Open(self, force=False, exclusive=True):
342 """Make the device ready for I/O.
343
344 This is a no-op for the file type.
345
346 """
347 assert self.attached, "Gluster file opened without being attached"
348
350 """Notifies that the device will no longer be used for I/O.
351
352 This is a no-op for the file type.
353 """
354 pass
355
357 """Remove the file backing the block device.
358
359 @rtype: boolean
360 @return: True if the removal was successful
361
362 """
363 with self.volume.Mount():
364 self.file = FileDeviceHelper(self.full_path)
365 if self.file.Remove():
366 self.file = None
367 return True
368 else:
369 return False
370
372 """Renames the file.
373
374 """
375
376 base.ThrowError("Rename is not supported for Gluster storage")
377
378 - def Grow(self, amount, dryrun, backingstore, excl_stor):
379 """Grow the file
380
381 @param amount: the amount (in mebibytes) to grow with
382
383 """
384 self.file.Grow(amount, dryrun, backingstore, excl_stor)
385
387 """Attach to an existing file.
388
389 Check if this file already exists.
390
391 @rtype: boolean
392 @return: True if file exists
393
394 """
395 try:
396 self.volume.Mount()
397 self.file = FileDeviceHelper(self.full_path)
398 self.dev_path = self.full_path
399 except Exception as err:
400 self.volume.Unmount()
401 raise err
402
403 self.attached = self.file.Exists()
404 return self.attached
405
407 """Return the actual disk size.
408
409 @note: the device needs to be active when this is called
410
411 """
412 return self.file.Size()
413
415 """Generate KVM userspace URIs to be used as `-drive file` settings.
416
417 @see: L{BlockDev.GetUserspaceAccessUri}
418 @see: https://github.com/qemu/qemu/commit/8d6d89cb63c57569864ecdeb84d3a1c2eb
419 """
420
421 if hypervisor == constants.HT_KVM:
422 return self.volume.GetKVMMountString(self.path)
423 else:
424 base.ThrowError("Hypervisor %s doesn't support Gluster userspace access" %
425 hypervisor)
426
427 @classmethod
428 - def Create(cls, unique_id, children, size, spindles, params, excl_stor,
429 dyn_params, *args):
430 """Create a new file.
431
432 @param size: the size of file in MiB
433
434 @rtype: L{bdev.FileStorage}
435 @return: an instance of FileStorage
436
437 """
438 if excl_stor:
439 raise errors.ProgrammerError("FileStorage device requested with"
440 " exclusive_storage")
441 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
442 raise ValueError("Invalid configuration data %s" % str(unique_id))
443
444 full_path = unique_id[1]
445
446 server_addr = params[constants.GLUSTER_HOST]
447 port = params[constants.GLUSTER_PORT]
448 volume = params[constants.GLUSTER_VOLUME]
449
450 volume_obj = GlusterVolume(server_addr, port, volume)
451 full_path = io.PathJoin(volume_obj.mount_point, full_path)
452
453
454
455 with volume_obj.Mount():
456 FileDeviceHelper.CreateFile(full_path, size, create_folders=True)
457
458 return GlusterStorage(unique_id, children, size, params, dyn_params,
459 *args)
460