Package ganeti :: Package storage :: Module gluster
[hide private]
[frames] | no frames]

Source Code for Module ganeti.storage.gluster

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2013 Google Inc. 
  5  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions are 
  9  # met: 
 10  # 
 11  # 1. Redistributions of source code must retain the above copyright notice, 
 12  # this list of conditions and the following disclaimer. 
 13  # 
 14  # 2. Redistributions in binary form must reproduce the above copyright 
 15  # notice, this list of conditions and the following disclaimer in the 
 16  # documentation and/or other materials provided with the distribution. 
 17  # 
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 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 
50 51 52 -class GlusterVolume(object):
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
66 - def __init__(self, server_addr, port, volume, _run_cmd=utils.RunCmd, 67 _mount_point=None):
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: # tests 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
94 - def server_ip(self):
95 return self._server_ip
96 97 @property
98 - def port(self):
99 return self._port
100 101 @property
102 - def volume(self):
103 return self._volume
104
105 - def __eq__(self, other):
106 return (self.server_ip, self.port, self.volume) == \ 107 (other.server_ip, other.port, other.volume)
108
109 - def __repr__(self):
110 return """GlusterVolume("{ip}", {port}, "{volume}")""" \ 111 .format(ip=self.server_ip, port=self.port, volume=self.volume)
112
113 - def __hash__(self):
114 return (self.server_ip, self.port, self.volume).__hash__()
115
116 - def _IsMounted(self):
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
128 - def _GuessMountFailReasons(self):
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 # Does the mount point exist? 140 if not os.path.exists(self.mount_point): 141 reasons.append("%r: does not exist" % self.mount_point) 142 143 # Okay, it exists, but is it a directory? 144 elif not os.path.isdir(self.mount_point): 145 reasons.append("%r: not a directory" % self.mount_point) 146 147 # If, for some unfortunate reason, this folder exists before mounting: 148 # 149 # /var/run/ganeti/gluster/gv0/10.0.0.1:30000:gv0/ 150 # '--------- cwd ------------' 151 # 152 # and you _are_ trying to mount the gluster volume gv0 on 10.0.0.1:30000, 153 # then the mount.glusterfs command parser gets confused and this command: 154 # 155 # mount -t glusterfs 10.0.0.1:30000:gv0 /var/run/ganeti/gluster/gv0 156 # '-- remote end --' '------ mountpoint -------' 157 # 158 # gets parsed instead like this: 159 # 160 # mount -t glusterfs 10.0.0.1:30000:gv0 /var/run/ganeti/gluster/gv0 161 # '-- mountpoint --' '----- syntax error ------' 162 # 163 # and if there _is_ a gluster server running locally at the default remote 164 # end, localhost:24007, then this is not a network error and therefore... no 165 # usage message gets printed out. All you get is a Byson parser error in the 166 # gluster log files about an unexpected token in line 1, "". (That's stdin.) 167 # 168 # Not that we rely on that output in any way whatsoever... 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 # Let's try something else: can we connect to the server? 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
189 - def _GetFUSEMountString(self):
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
198 - def GetKVMMountString(self, path):
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
210 - def Mount(self):
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 # We're already mounted. 232 return self
233 234 def __exit__(self, *exception_information): 235 self.volume.Unmount() 236 return False # do not swallow exceptions. 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 # Why set cwd? Because it's an area we control. If, 249 # for some unfortunate reason, this folder exists: 250 # "/%s/" % _GetFUSEMountString() 251 # ...then the gluster parser gets confused and treats 252 # _GetFUSEMountString() as your mount point and 253 # self.mount_point becomes a syntax error. 254 cwd=self.mount_point) 255 256 # mount.glusterfs exits with code 0 even after failure. 257 # https://bugzilla.redhat.com/show_bug.cgi?id=1031973 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
268 - def Unmount(self):
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
286 287 -class GlusterStorage(base.BlockDev):
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: # wrong number of arguments 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
323 - def Assemble(self):
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
332 - def Shutdown(self):
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
349 - def Close(self):
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
356 - def Remove(self):
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
371 - def Rename(self, new_id):
372 """Renames the file. 373 374 """ 375 # TODO: implement rename for file-based storage 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
386 - def Attach(self):
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
406 - def GetActualSize(self):
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
414 - def GetUserspaceAccessUri(self, hypervisor):
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 # Possible optimization: defer actual creation to first Attach, rather 454 # than mounting and unmounting here, then remounting immediately after. 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