Statistics
| Branch: | Tag: | Revision:

root / lib / storage / gluster.py @ ac156ecd

History | View | Annotate | Download (11.6 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

    
37
from ganeti.utils import io
38
from ganeti.storage import base
39
from ganeti.storage.filestorage import FileDeviceHelper
40

    
41

    
42
class GlusterVolume(object):
43
  """This class represents a Gluster volume.
44

45
  Volumes are uniquely identified by:
46

47
    - their IP address
48
    - their port
49
    - the volume name itself
50

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

54
  """
55

    
56
  def __init__(self, server_addr, port, volume, _run_cmd=utils.RunCmd,
57
               _mount_point=None):
58
    """Creates a Gluster volume object.
59

60
    @type server_addr: str
61
    @param server_addr: The address to connect to
62

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

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

69
    """
70
    self.server_addr = server_addr
71
    server_ip = netutils.Hostname.GetIP(self.server_addr)
72
    self._server_ip = server_ip
73
    port = netutils.ValidatePortNumber(port)
74
    self._port = port
75
    self._volume = volume
76
    if _mount_point: # tests
77
      self.mount_point = _mount_point
78
    else:
79
      self.mount_point = ssconf.SimpleStore().GetGlusterStorageDir()
80

    
81
    self._run_cmd = _run_cmd
82

    
83
  @property
84
  def server_ip(self):
85
    return self._server_ip
86

    
87
  @property
88
  def port(self):
89
    return self._port
90

    
91
  @property
92
  def volume(self):
93
    return self._volume
94

    
95
  def __eq__(self, other):
96
    return (self.server_ip, self.port, self.volume) == \
97
           (other.server_ip, other.port, other.volume)
98

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

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

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

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

112
    """
113
    if not os.path.exists(self.mount_point):
114
      return False
115

    
116
    return os.path.ismount(self.mount_point)
117

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

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

125
    """
126

    
127
    reasons = []
128

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

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

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

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

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

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

    
177
    return "; ".join(reasons)
178

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

182
    @rtype: str
183
    """
184

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

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

191
    @rtype: str
192
    """
193

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

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

203
    @raises BlockDeviceError: if the mount was unsuccessful
204

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

209
              with volume.mount():
210
                # Do operations on volume
211
              # Volume is now unmounted
212

213
    """
214

    
215
    class _GlusterVolumeContextManager(object):
216

    
217
      def __init__(self, volume):
218
        self.volume = volume
219

    
220
      def __enter__(self):
221
        # We're already mounted.
222
        return self
223

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

    
228
    if self._IsMounted():
229
      return _GlusterVolumeContextManager(self)
230

    
231
    command = ["mount",
232
               "-t", "glusterfs",
233
               self._GetFUSEMountString(),
234
               self.mount_point]
235

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

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

    
256
    return _GlusterVolumeContextManager(self)
257

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

261
    Failures are logged but otherwise ignored.
262

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

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

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

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

    
276

    
277
class GlusterStorage(base.BlockDev):
278
  """File device using the Gluster backend.
279

280
  This class represents a file storage backend device stored on Gluster. The
281
  system administrator must mount the Gluster device himself at boot time before
282
  Ganeti is run.
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
    super(GlusterStorage, self).__init__(unique_id, children, size, params,
294
                                         dyn_params)
295
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
296
      raise ValueError("Invalid configuration data %s" % str(unique_id))
297
    self.driver = unique_id[0]
298
    self.dev_path = unique_id[1]
299

    
300
    self.file = FileDeviceHelper(self.dev_path)
301

    
302
    self.Attach()
303

    
304
  def Assemble(self):
305
    """Assemble the device.
306

307
    Checks whether the file device exists, raises BlockDeviceError otherwise.
308

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

    
313
  def Shutdown(self):
314
    """Shutdown the device.
315

316
    """
317

    
318
    self.file = None
319
    self.dev_path = None
320
    self.attached = False
321

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

325
    This is a no-op for the file type.
326

327
    """
328
    assert self.attached, "Gluster file opened without being attached"
329

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

333
    This is a no-op for the file type.
334
    """
335
    pass
336

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

340
    @rtype: boolean
341
    @return: True if the removal was successful
342

343
    """
344
    return self.file.Remove()
345

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

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

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

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

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

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

364
    Check if this file already exists.
365

366
    @rtype: boolean
367
    @return: True if file exists
368

369
    """
370
    self.attached = self.file.Exists()
371
    return self.attached
372

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

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

378
    """
379
    return self.file.Size()
380

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

386
    @param size: the size of file in MiB
387

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

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

    
398
    dev_path = unique_id[1]
399

    
400
    FileDeviceHelper.Create(dev_path, size)
401
    return GlusterStorage(unique_id, children, size, params, dyn_params)