gluster.py 12.7 KB
Newer Older
Santi Raffa's avatar
Santi Raffa committed
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
#
#

# Copyright (C) 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.

"""Gluster storage class.

This class is very similar to FileStorage, given that Gluster when mounted
behaves essentially like a regular file system. Unlike RBD, there are no
special provisions for block device abstractions (yet).

"""
Santi Raffa's avatar
Santi Raffa committed
28
29
30
31
32
import logging
import os
import socket

from ganeti import utils
Santi Raffa's avatar
Santi Raffa committed
33
from ganeti import errors
Santi Raffa's avatar
Santi Raffa committed
34
35
from ganeti import netutils
from ganeti import constants
Santi Raffa's avatar
Santi Raffa committed
36
from ganeti import ssconf
Santi Raffa's avatar
Santi Raffa committed
37

Santi Raffa's avatar
Santi Raffa committed
38
from ganeti.utils import io
Santi Raffa's avatar
Santi Raffa committed
39
40
41
42
from ganeti.storage import base
from ganeti.storage.filestorage import FileDeviceHelper


Santi Raffa's avatar
Santi Raffa committed
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class GlusterVolume(object):
  """This class represents a Gluster volume.

  Volumes are uniquely identified by:

    - their IP address
    - their port
    - the volume name itself

  Two GlusterVolume objects x, y with same IP address, port and volume name
  are considered equal.

  """

57
58
  def __init__(self, server_addr, port, volume, _run_cmd=utils.RunCmd,
               _mount_point=None):
Santi Raffa's avatar
Santi Raffa committed
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
    """Creates a Gluster volume object.

    @type server_addr: str
    @param server_addr: The address to connect to

    @type port: int
    @param port: The port to connect to (Gluster standard is 24007)

    @type volume: str
    @param volume: The gluster volume to use for storage.

    """
    self.server_addr = server_addr
    server_ip = netutils.Hostname.GetIP(self.server_addr)
    self._server_ip = server_ip
    port = netutils.ValidatePortNumber(port)
    self._port = port
    self._volume = volume
77
78
79
80
    if _mount_point: # tests
      self.mount_point = _mount_point
    else:
      self.mount_point = ssconf.SimpleStore().GetGlusterStorageDir()
Santi Raffa's avatar
Santi Raffa committed
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277

    self._run_cmd = _run_cmd

  @property
  def server_ip(self):
    return self._server_ip

  @property
  def port(self):
    return self._port

  @property
  def volume(self):
    return self._volume

  def __eq__(self, other):
    return (self.server_ip, self.port, self.volume) == \
           (other.server_ip, other.port, other.volume)

  def __repr__(self):
    return """GlusterVolume("{ip}", {port}, "{volume}")""" \
             .format(ip=self.server_ip, port=self.port, volume=self.volume)

  def __hash__(self):
    return (self.server_ip, self.port, self.volume).__hash__()

  def _IsMounted(self):
    """Checks if we are mounted or not.

    @rtype: bool
    @return: True if this volume is mounted.

    """
    if not os.path.exists(self.mount_point):
      return False

    return os.path.ismount(self.mount_point)

  def _GuessMountFailReasons(self):
    """Try and give reasons why the mount might've failed.

    @rtype: str
    @return: A semicolon-separated list of problems found with the current setup
             suitable for display to the user.

    """

    reasons = []

    # Does the mount point exist?
    if not os.path.exists(self.mount_point):
      reasons.append("%r: does not exist" % self.mount_point)

    # Okay, it exists, but is it a directory?
    elif not os.path.isdir(self.mount_point):
      reasons.append("%r: not a directory" % self.mount_point)

    # If, for some unfortunate reason, this folder exists before mounting:
    #
    #   /var/run/ganeti/gluster/gv0/10.0.0.1:30000:gv0/
    #   '--------- cwd ------------'
    #
    # and you _are_ trying to mount the gluster volume gv0 on 10.0.0.1:30000,
    # then the mount.glusterfs command parser gets confused and this command:
    #
    #   mount -t glusterfs 10.0.0.1:30000:gv0 /var/run/ganeti/gluster/gv0
    #                      '-- remote end --' '------ mountpoint -------'
    #
    # gets parsed instead like this:
    #
    #   mount -t glusterfs 10.0.0.1:30000:gv0 /var/run/ganeti/gluster/gv0
    #                      '-- mountpoint --' '----- syntax error ------'
    #
    # and if there _is_ a gluster server running locally at the default remote
    # end, localhost:24007, then this is not a network error and therefore... no
    # usage message gets printed out. All you get is a Byson parser error in the
    # gluster log files about an unexpected token in line 1, "". (That's stdin.)
    #
    # Not that we rely on that output in any way whatsoever...

    parser_confusing = io.PathJoin(self.mount_point,
                                   self._GetFUSEMountString())
    if os.path.exists(parser_confusing):
      reasons.append("%r: please delete, rename or move." % parser_confusing)

    # Let's try something else: can we connect to the server?
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
      sock.connect((self.server_ip, self.port))
      sock.close()
    except socket.error as err:
      reasons.append("%s:%d: %s" % (self.server_ip, self.port, err.strerror))

    reasons.append("try running 'gluster volume info %s' on %s to ensure"
                   " it exists, it is started and it is using the tcp"
                   " transport" % (self.volume, self.server_ip))

    return "; ".join(reasons)

  def _GetFUSEMountString(self):
    """Return the string FUSE needs to mount this volume.

    @rtype: str
    """

    return "{ip}:{port}:{volume}" \
              .format(ip=self.server_ip, port=self.port, volume=self.volume)

  def GetKVMMountString(self, path):
    """Return the string KVM needs to use this volume.

    @rtype: str
    """

    ip = self.server_ip
    if netutils.IPAddress.GetAddressFamily(ip) == socket.AF_INET6:
      ip = "[%s]" % ip
    return "gluster://{ip}:{port}/{volume}/{path}" \
              .format(ip=ip, port=self.port, volume=self.volume, path=path)

  def Mount(self):
    """Try and mount the volume. No-op if the volume is already mounted.

    @raises BlockDeviceError: if the mount was unsuccessful

    @rtype: context manager
    @return: A simple context manager that lets you use this volume for
             short lived operations like so::

              with volume.mount():
                # Do operations on volume
              # Volume is now unmounted

    """

    class _GlusterVolumeContextManager(object):

      def __init__(self, volume):
        self.volume = volume

      def __enter__(self):
        # We're already mounted.
        return self

      def __exit__(self, *exception_information):
        self.volume.Unmount()
        return False # do not swallow exceptions.

    if self._IsMounted():
      return _GlusterVolumeContextManager(self)

    command = ["mount",
               "-t", "glusterfs",
               self._GetFUSEMountString(),
               self.mount_point]

    io.Makedirs(self.mount_point)
    self._run_cmd(" ".join(command),
                  # Why set cwd? Because it's an area we control. If,
                  # for some unfortunate reason, this folder exists:
                  #   "/%s/" % _GetFUSEMountString()
                  # ...then the gluster parser gets confused and treats
                  # _GetFUSEMountString() as your mount point and
                  # self.mount_point becomes a syntax error.
                  cwd=self.mount_point)

    # mount.glusterfs exits with code 0 even after failure.
    # https://bugzilla.redhat.com/show_bug.cgi?id=1031973
    if not self._IsMounted():
      reasons = self._GuessMountFailReasons()
      if not reasons:
        reasons = "%r failed." % (" ".join(command))
      base.ThrowError("%r: mount failure: %s",
                      self.mount_point,
                      reasons)

    return _GlusterVolumeContextManager(self)

  def Unmount(self):
    """Try and unmount the volume.

    Failures are logged but otherwise ignored.

    @raises BlockDeviceError: if the volume was not mounted to begin with.
    """

    if not self._IsMounted():
      base.ThrowError("%r: should be mounted but isn't.", self.mount_point)

    result = self._run_cmd(["umount",
                            self.mount_point])

    if result.failed:
      logging.warning("Failed to unmount %r from %r: %s",
                      self, self.mount_point, result.fail_reason)


Santi Raffa's avatar
Santi Raffa committed
278
279
280
class GlusterStorage(base.BlockDev):
  """File device using the Gluster backend.

Santi Raffa's avatar
Santi Raffa committed
281
282
  This class represents a file storage backend device stored on Gluster. Ganeti
  mounts and unmounts the Gluster devices automatically.
Santi Raffa's avatar
Santi Raffa committed
283
284
285
286
287
288
289
290
291
292
293

  The unique_id for the file device is a (file_driver, file_path) tuple.

  """
  def __init__(self, unique_id, children, size, params, dyn_params):
    """Initalizes a file device backend.

    """
    if children:
      base.ThrowError("Invalid setup for file device")

Santi Raffa's avatar
Santi Raffa committed
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
    try:
      driver, path = unique_id
    except ValueError: # wrong number of arguments
      raise ValueError("Invalid configuration data %s" % repr(unique_id))

    server_addr = params[constants.GLUSTER_HOST]
    port = params[constants.GLUSTER_PORT]
    volume = params[constants.GLUSTER_VOLUME]

    self.volume = GlusterVolume(server_addr, port, volume)
    self.path = path
    self.driver = driver
    self.full_path = io.PathJoin(self.volume.mount_point, self.path)
    self.file = None

    super(GlusterStorage, self).__init__(unique_id, children, size,
                                         params, dyn_params)
Santi Raffa's avatar
Santi Raffa committed
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353

    self.Attach()

  def Assemble(self):
    """Assemble the device.

    Checks whether the file device exists, raises BlockDeviceError otherwise.

    """
    assert self.attached, "Gluster file assembled without being attached"
    self.file.Exists(assert_exists=True)

  def Shutdown(self):
    """Shutdown the device.

    """

    self.file = None
    self.dev_path = None
    self.attached = False

  def Open(self, force=False):
    """Make the device ready for I/O.

    This is a no-op for the file type.

    """
    assert self.attached, "Gluster file opened without being attached"

  def Close(self):
    """Notifies that the device will no longer be used for I/O.

    This is a no-op for the file type.
    """
    pass

  def Remove(self):
    """Remove the file backing the block device.

    @rtype: boolean
    @return: True if the removal was successful

    """
Santi Raffa's avatar
Santi Raffa committed
354
355
356
357
358
359
360
    with self.volume.Mount():
      self.file = FileDeviceHelper(self.full_path)
      if self.file.Remove():
        self.file = None
        return True
      else:
        return False
Santi Raffa's avatar
Santi Raffa committed
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385

  def Rename(self, new_id):
    """Renames the file.

    """
    # TODO: implement rename for file-based storage
    base.ThrowError("Rename is not supported for Gluster storage")

  def Grow(self, amount, dryrun, backingstore, excl_stor):
    """Grow the file

    @param amount: the amount (in mebibytes) to grow with

    """
    self.file.Grow(amount, dryrun, backingstore, excl_stor)

  def Attach(self):
    """Attach to an existing file.

    Check if this file already exists.

    @rtype: boolean
    @return: True if file exists

    """
Santi Raffa's avatar
Santi Raffa committed
386
387
388
389
390
391
392
393
    try:
      self.volume.Mount()
      self.file = FileDeviceHelper(self.full_path)
      self.dev_path = self.full_path
    except Exception as err:
      self.volume.Unmount()
      raise err

Santi Raffa's avatar
Santi Raffa committed
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
    self.attached = self.file.Exists()
    return self.attached

  def GetActualSize(self):
    """Return the actual disk size.

    @note: the device needs to be active when this is called

    """
    return self.file.Size()

  @classmethod
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
             dyn_params):
    """Create a new file.

    @param size: the size of file in MiB

    @rtype: L{bdev.FileStorage}
    @return: an instance of FileStorage

    """
    if excl_stor:
      raise errors.ProgrammerError("FileStorage device requested with"
                                   " exclusive_storage")
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
      raise ValueError("Invalid configuration data %s" % str(unique_id))

Santi Raffa's avatar
Santi Raffa committed
422
423
424
425
426
427
428
429
430
431
432
433
434
    full_path = unique_id[1]

    server_addr = params[constants.GLUSTER_HOST]
    port = params[constants.GLUSTER_PORT]
    volume = params[constants.GLUSTER_VOLUME]

    volume_obj = GlusterVolume(server_addr, port, volume)
    full_path = io.PathJoin(volume_obj.mount_point, full_path)

    # Possible optimization: defer actual creation to first Attach, rather
    # than mounting and unmounting here, then remounting immediately after.
    with volume_obj.Mount():
      FileDeviceHelper.CreateFile(full_path, size, create_folders=True)
Santi Raffa's avatar
Santi Raffa committed
435
436

    return GlusterStorage(unique_id, children, size, params, dyn_params)