Statistics
| Branch: | Tag: | Revision:

root / lib / storage / filestorage.py @ bddc92ee

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

    
22
"""Filesystem-based access functions and disk templates.
23

24
"""
25

    
26
import logging
27
import errno
28
import os
29

    
30
from ganeti import compat
31
from ganeti import constants
32
from ganeti import errors
33
from ganeti import pathutils
34
from ganeti import utils
35
from ganeti.utils import io
36
from ganeti.storage import base
37

    
38

    
39
class FileDeviceHelper(object):
40

    
41
  @classmethod
42
  def CreateFile(cls, path, size, create_folders=False,
43
                 _file_path_acceptance_fn=None):
44
    """Create a new file and its file device helper.
45

46
    @param size: the size in MiBs the file should be truncated to.
47
    @param create_folders: create the directories for the path if necessary
48
                           (using L{ganeti.utils.io.Makedirs})
49

50
    @rtype: FileDeviceHelper
51
    @return: The FileDeviceHelper object representing the object.
52
    @raise errors.FileStoragePathError: if the file path is disallowed by policy
53

54
    """
55

    
56
    if not _file_path_acceptance_fn:
57
      _file_path_acceptance_fn = CheckFileStoragePathAcceptance
58
    _file_path_acceptance_fn(path)
59

    
60
    if create_folders:
61
      folder = os.path.dirname(path)
62
      io.Makedirs(folder)
63

    
64
    try:
65
      fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
66
      f = os.fdopen(fd, "w")
67
      f.truncate(size * 1024 * 1024)
68
      f.close()
69
    except EnvironmentError as err:
70
      base.ThrowError("%s: can't create: %s", path, str(err))
71

    
72
    return FileDeviceHelper(path,
73
                            _file_path_acceptance_fn=_file_path_acceptance_fn)
74

    
75
  def __init__(self, path, _file_path_acceptance_fn=None):
76
    """Create a new file device helper.
77

78
    @raise errors.FileStoragePathError: if the file path is disallowed by policy
79

80
    """
81
    if not _file_path_acceptance_fn:
82
      _file_path_acceptance_fn = CheckFileStoragePathAcceptance
83
    _file_path_acceptance_fn(path)
84

    
85
    self.path = path
86

    
87
  def Exists(self, assert_exists=None):
88
    """Check for the existence of the given file.
89

90
    @param assert_exists: creates an assertion on the result value:
91
      - if true, raise errors.BlockDeviceError if the file doesn't exist
92
      - if false, raise errors.BlockDeviceError if the file does exist
93
    @rtype: boolean
94
    @return: True if the file exists
95

96
    """
97

    
98
    exists = os.path.isfile(self.path)
99

    
100
    if not exists and assert_exists is True:
101
      raise base.ThrowError("%s: No such file", self.path)
102
    if exists and assert_exists is False:
103
      raise base.ThrowError("%s: File exists", self.path)
104

    
105
    return exists
106

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

110
    @rtype: boolean
111
    @return: True if the removal was successful
112

113
    """
114
    try:
115
      os.remove(self.path)
116
      return True
117
    except OSError as err:
118
      if err.errno != errno.ENOENT:
119
        base.ThrowError("%s: can't remove: %s", self.path, err)
120
      return False
121

    
122
  def Size(self):
123
    """Return the actual disk size in bytes.
124

125
    @rtype: int
126
    @return: The file size in bytes.
127

128
    """
129
    self.Exists(assert_exists=True)
130
    try:
131
      return os.stat(self.path).st_size
132
    except OSError as err:
133
      base.ThrowError("%s: can't stat: %s", self.path, err)
134

    
135
  def Grow(self, amount, dryrun, backingstore, _excl_stor):
136
    """Grow the file
137

138
    @param amount: the amount (in mebibytes) to grow by.
139

140
    """
141
    # Check that the file exists
142
    self.Exists(assert_exists=True)
143

    
144
    if amount < 0:
145
      base.ThrowError("%s: can't grow by negative amount", self.path)
146

    
147
    if dryrun:
148
      return
149
    if not backingstore:
150
      return
151

    
152
    current_size = self.Size()
153
    new_size = current_size + amount * 1024 * 1024
154
    try:
155
      f = open(self.path, "a+")
156
      f.truncate(new_size)
157
      f.close()
158
    except EnvironmentError, err:
159
      base.ThrowError("%s: can't grow: ", self.path, str(err))
160

    
161

    
162
class FileStorage(base.BlockDev):
163
  """File device.
164

165
  This class represents a file storage backend device.
166

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

169
  """
170
  def __init__(self, unique_id, children, size, params, dyn_params, *args):
171
    """Initalizes a file device backend.
172

173
    """
174
    if children:
175
      raise errors.BlockDeviceError("Invalid setup for file device")
176
    super(FileStorage, self).__init__(unique_id, children, size, params,
177
                                      dyn_params, *args)
178
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
179
      raise ValueError("Invalid configuration data %s" % str(unique_id))
180
    self.driver = unique_id[0]
181
    self.dev_path = unique_id[1]
182
    self.file = FileDeviceHelper(self.dev_path)
183
    self.Attach()
184

    
185
  def Assemble(self):
186
    """Assemble the device.
187

188
    Checks whether the file device exists, raises BlockDeviceError otherwise.
189

190
    """
191
    self.file.Exists(assert_exists=True)
192

    
193
  def Shutdown(self):
194
    """Shutdown the device.
195

196
    This is a no-op for the file type, as we don't deactivate
197
    the file on shutdown.
198

199
    """
200
    pass
201

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

205
    This is a no-op for the file type.
206

207
    """
208
    pass
209

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

213
    This is a no-op for the file type.
214

215
    """
216
    pass
217

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

221
    @rtype: boolean
222
    @return: True if the removal was successful
223

224
    """
225
    return self.file.Remove()
226

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

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

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

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

239
    """
240
    if not backingstore:
241
      return
242
    if dryrun:
243
      return
244
    self.file.Grow(amount, dryrun, backingstore, excl_stor)
245

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

249
    Check if this file already exists.
250

251
    @rtype: boolean
252
    @return: True if file exists
253

254
    """
255
    self.attached = self.file.Exists()
256
    return self.attached
257

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

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

263
    """
264
    return self.file.Size()
265

    
266
  @classmethod
267
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
268
             dyn_params, *args):
269
    """Create a new file.
270

271
    @type size: int
272
    @param size: the size of file in MiB
273

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

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

    
284
    dev_path = unique_id[1]
285

    
286
    FileDeviceHelper.CreateFile(dev_path, size)
287
    return FileStorage(unique_id, children, size, params, dyn_params,
288
                       *args)
289

    
290

    
291
def GetFileStorageSpaceInfo(path):
292
  """Retrieves the free and total space of the device where the file is
293
     located.
294

295
     @type path: string
296
     @param path: Path of the file whose embracing device's capacity is
297
       reported.
298
     @return: a dictionary containing 'vg_size' and 'vg_free' given in MebiBytes
299

300
  """
301
  try:
302
    result = os.statvfs(path)
303
    free = (result.f_frsize * result.f_bavail) / (1024 * 1024)
304
    size = (result.f_frsize * result.f_blocks) / (1024 * 1024)
305
    return {"type": constants.ST_FILE,
306
            "name": path,
307
            "storage_size": size,
308
            "storage_free": free}
309
  except OSError, e:
310
    raise errors.CommandError("Failed to retrieve file system information about"
311
                              " path: %s - %s" % (path, e.strerror))
312

    
313

    
314
def _GetForbiddenFileStoragePaths():
315
  """Builds a list of path prefixes which shouldn't be used for file storage.
316

317
  @rtype: frozenset
318

319
  """
320
  paths = set([
321
    "/boot",
322
    "/dev",
323
    "/etc",
324
    "/home",
325
    "/proc",
326
    "/root",
327
    "/sys",
328
    ])
329

    
330
  for prefix in ["", "/usr", "/usr/local"]:
331
    paths.update(map(lambda s: "%s/%s" % (prefix, s),
332
                     ["bin", "lib", "lib32", "lib64", "sbin"]))
333

    
334
  return compat.UniqueFrozenset(map(os.path.normpath, paths))
335

    
336

    
337
def _ComputeWrongFileStoragePaths(paths,
338
                                  _forbidden=_GetForbiddenFileStoragePaths()):
339
  """Cross-checks a list of paths for prefixes considered bad.
340

341
  Some paths, e.g. "/bin", should not be used for file storage.
342

343
  @type paths: list
344
  @param paths: List of paths to be checked
345
  @rtype: list
346
  @return: Sorted list of paths for which the user should be warned
347

348
  """
349
  def _Check(path):
350
    return (not os.path.isabs(path) or
351
            path in _forbidden or
352
            filter(lambda p: utils.IsBelowDir(p, path), _forbidden))
353

    
354
  return utils.NiceSort(filter(_Check, map(os.path.normpath, paths)))
355

    
356

    
357
def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE):
358
  """Returns a list of file storage paths whose prefix is considered bad.
359

360
  See L{_ComputeWrongFileStoragePaths}.
361

362
  """
363
  return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename))
364

    
365

    
366
def _CheckFileStoragePath(path, allowed, exact_match_ok=False):
367
  """Checks if a path is in a list of allowed paths for file storage.
368

369
  @type path: string
370
  @param path: Path to check
371
  @type allowed: list
372
  @param allowed: List of allowed paths
373
  @type exact_match_ok: bool
374
  @param exact_match_ok: whether or not it is okay when the path is exactly
375
      equal to an allowed path and not a subdir of it
376
  @raise errors.FileStoragePathError: If the path is not allowed
377

378
  """
379
  if not os.path.isabs(path):
380
    raise errors.FileStoragePathError("File storage path must be absolute,"
381
                                      " got '%s'" % path)
382

    
383
  for i in allowed:
384
    if not os.path.isabs(i):
385
      logging.info("Ignoring relative path '%s' for file storage", i)
386
      continue
387

    
388
    if exact_match_ok:
389
      if os.path.normpath(i) == os.path.normpath(path):
390
        break
391

    
392
    if utils.IsBelowDir(i, path):
393
      break
394
  else:
395
    raise errors.FileStoragePathError("Path '%s' is not acceptable for file"
396
                                      " storage" % path)
397

    
398

    
399
def _LoadAllowedFileStoragePaths(filename):
400
  """Loads file containing allowed file storage paths.
401

402
  @rtype: list
403
  @return: List of allowed paths (can be an empty list)
404

405
  """
406
  try:
407
    contents = utils.ReadFile(filename)
408
  except EnvironmentError:
409
    return []
410
  else:
411
    return utils.FilterEmptyLinesAndComments(contents)
412

    
413

    
414
def CheckFileStoragePathAcceptance(
415
    path, _filename=pathutils.FILE_STORAGE_PATHS_FILE,
416
    exact_match_ok=False):
417
  """Checks if a path is allowed for file storage.
418

419
  @type path: string
420
  @param path: Path to check
421
  @raise errors.FileStoragePathError: If the path is not allowed
422

423
  """
424
  allowed = _LoadAllowedFileStoragePaths(_filename)
425
  if not allowed:
426
    raise errors.FileStoragePathError("No paths are valid or path file '%s'"
427
                                      " was not accessible." % _filename)
428

    
429
  if _ComputeWrongFileStoragePaths([path]):
430
    raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" %
431
                                      path)
432

    
433
  _CheckFileStoragePath(path, allowed, exact_match_ok=exact_match_ok)
434

    
435

    
436
def _CheckFileStoragePathExistance(path):
437
  """Checks whether the given path is usable on the file system.
438

439
  This checks wether the path is existing, a directory and writable.
440

441
  @type path: string
442
  @param path: path to check
443

444
  """
445
  if not os.path.isdir(path):
446
    raise errors.FileStoragePathError("Path '%s' is not existing or not a"
447
                                      " directory." % path)
448
  if not os.access(path, os.W_OK):
449
    raise errors.FileStoragePathError("Path '%s' is not writable" % path)
450

    
451

    
452
def CheckFileStoragePath(
453
    path, _allowed_paths_file=pathutils.FILE_STORAGE_PATHS_FILE):
454
  """Checks whether the path exists and is acceptable to use.
455

456
  Can be used for any file-based storage, for example shared-file storage.
457

458
  @type path: string
459
  @param path: path to check
460
  @rtype: string
461
  @returns: error message if the path is not ready to use
462

463
  """
464
  try:
465
    CheckFileStoragePathAcceptance(path, _filename=_allowed_paths_file,
466
                                   exact_match_ok=True)
467
  except errors.FileStoragePathError as e:
468
    return str(e)
469
  if not os.path.isdir(path):
470
    return "Path '%s' is not exisiting or not a directory." % path
471
  if not os.access(path, os.W_OK):
472
    return "Path '%s' is not writable" % path