Revision 58793040 lib/storage/gluster.py

b/lib/storage/gluster.py
25 25
special provisions for block device abstractions (yet).
26 26

  
27 27
"""
28
import logging
29
import os
30
import socket
31

  
32
from ganeti import utils
28 33
from ganeti import errors
34
from ganeti import netutils
35
from ganeti import constants
29 36

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

  
33 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
    """Creates a Gluster volume object.
58

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

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

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

  
68
    """
69
    self.server_addr = server_addr
70
    server_ip = netutils.Hostname.GetIP(self.server_addr)
71
    self._server_ip = server_ip
72
    port = netutils.ValidatePortNumber(port)
73
    self._port = port
74
    self._volume = volume
75
    self.mount_point = io.PathJoin(constants.GLUSTER_MOUNTPOINT,
76
                                   self._volume)
77

  
78
    self._run_cmd = _run_cmd
79

  
80
  @property
81
  def server_ip(self):
82
    return self._server_ip
83

  
84
  @property
85
  def port(self):
86
    return self._port
87

  
88
  @property
89
  def volume(self):
90
    return self._volume
91

  
92
  def __eq__(self, other):
93
    return (self.server_ip, self.port, self.volume) == \
94
           (other.server_ip, other.port, other.volume)
95

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

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

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

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

  
109
    """
110
    if not os.path.exists(self.mount_point):
111
      return False
112

  
113
    return os.path.ismount(self.mount_point)
114

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

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

  
122
    """
123

  
124
    reasons = []
125

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

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

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

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

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

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

  
174
    return "; ".join(reasons)
175

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

  
179
    @rtype: str
180
    """
181

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

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

  
188
    @rtype: str
189
    """
190

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

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

  
200
    @raises BlockDeviceError: if the mount was unsuccessful
201

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

  
206
              with volume.mount():
207
                # Do operations on volume
208
              # Volume is now unmounted
209

  
210
    """
211

  
212
    class _GlusterVolumeContextManager(object):
213

  
214
      def __init__(self, volume):
215
        self.volume = volume
216

  
217
      def __enter__(self):
218
        # We're already mounted.
219
        return self
220

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

  
225
    if self._IsMounted():
226
      return _GlusterVolumeContextManager(self)
227

  
228
    command = ["mount",
229
               "-t", "glusterfs",
230
               self._GetFUSEMountString(),
231
               self.mount_point]
232

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

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

  
253
    return _GlusterVolumeContextManager(self)
254

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

  
258
    Failures are logged but otherwise ignored.
259

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

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

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

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

  
273

  
34 274
class GlusterStorage(base.BlockDev):
35 275
  """File device using the Gluster backend.
36 276

  

Also available in: Unified diff