Statistics
| Branch: | Tag: | Revision:

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)