Statistics
| Branch: | Tag: | Revision:

root / lib / storage / filestorage.py @ c032b2ce

History | View | Annotate | Download (12.9 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):
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

    
289

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

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

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

    
312

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

316
  @rtype: frozenset
317

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

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

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

    
335

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

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

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

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

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

    
355

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

359
  See L{_ComputeWrongFileStoragePaths}.
360

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

    
364

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

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

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

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

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

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

    
397

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

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

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

    
412

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

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

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

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

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

    
434

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

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

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

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

    
450

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

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

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

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