Statistics
| Branch: | Tag: | Revision:

root / lib / storage / filestorage.py @ 2656b017

History | View | Annotate | Download (10.7 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.storage import base
36

    
37

    
38
class FileStorage(base.BlockDev):
39
  """File device.
40

41
  This class represents a file storage backend device.
42

43
  The unique_id for the file device is a (file_driver, file_path) tuple.
44

45
  """
46
  def __init__(self, unique_id, children, size, params, dyn_params):
47
    """Initalizes a file device backend.
48

49
    """
50
    if children:
51
      raise errors.BlockDeviceError("Invalid setup for file device")
52
    super(FileStorage, self).__init__(unique_id, children, size, params,
53
                                      dyn_params)
54
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
55
      raise ValueError("Invalid configuration data %s" % str(unique_id))
56
    self.driver = unique_id[0]
57
    self.dev_path = unique_id[1]
58

    
59
    CheckFileStoragePathAcceptance(self.dev_path)
60

    
61
    self.Attach()
62

    
63
  def Assemble(self):
64
    """Assemble the device.
65

66
    Checks whether the file device exists, raises BlockDeviceError otherwise.
67

68
    """
69
    if not os.path.exists(self.dev_path):
70
      base.ThrowError("File device '%s' does not exist" % self.dev_path)
71

    
72
  def Shutdown(self):
73
    """Shutdown the device.
74

75
    This is a no-op for the file type, as we don't deactivate
76
    the file on shutdown.
77

78
    """
79
    pass
80

    
81
  def Open(self, force=False):
82
    """Make the device ready for I/O.
83

84
    This is a no-op for the file type.
85

86
    """
87
    pass
88

    
89
  def Close(self):
90
    """Notifies that the device will no longer be used for I/O.
91

92
    This is a no-op for the file type.
93

94
    """
95
    pass
96

    
97
  def Remove(self):
98
    """Remove the file backing the block device.
99

100
    @rtype: boolean
101
    @return: True if the removal was successful
102

103
    """
104
    try:
105
      os.remove(self.dev_path)
106
    except OSError, err:
107
      if err.errno != errno.ENOENT:
108
        base.ThrowError("Can't remove file '%s': %s", self.dev_path, err)
109

    
110
  def Rename(self, new_id):
111
    """Renames the file.
112

113
    """
114
    # TODO: implement rename for file-based storage
115
    base.ThrowError("Rename is not supported for file-based storage")
116

    
117
  def Grow(self, amount, dryrun, backingstore, excl_stor):
118
    """Grow the file
119

120
    @param amount: the amount (in mebibytes) to grow with
121

122
    """
123
    if not backingstore:
124
      return
125
    # Check that the file exists
126
    self.Assemble()
127
    current_size = self.GetActualSize()
128
    new_size = current_size + amount * 1024 * 1024
129
    assert new_size > current_size, "Cannot Grow with a negative amount"
130
    # We can't really simulate the growth
131
    if dryrun:
132
      return
133
    try:
134
      f = open(self.dev_path, "a+")
135
      f.truncate(new_size)
136
      f.close()
137
    except EnvironmentError, err:
138
      base.ThrowError("Error in file growth: %", str(err))
139

    
140
  def Attach(self):
141
    """Attach to an existing file.
142

143
    Check if this file already exists.
144

145
    @rtype: boolean
146
    @return: True if file exists
147

148
    """
149
    self.attached = os.path.exists(self.dev_path)
150
    return self.attached
151

    
152
  def GetActualSize(self):
153
    """Return the actual disk size.
154

155
    @note: the device needs to be active when this is called
156

157
    """
158
    assert self.attached, "BlockDevice not attached in GetActualSize()"
159
    try:
160
      st = os.stat(self.dev_path)
161
      return st.st_size
162
    except OSError, err:
163
      base.ThrowError("Can't stat %s: %s", self.dev_path, err)
164

    
165
  @classmethod
166
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
167
             dyn_params):
168
    """Create a new file.
169

170
    @type size: int
171
    @param size: the size of file in MiB
172

173
    @rtype: L{bdev.FileStorage}
174
    @return: an instance of FileStorage
175

176
    """
177
    if excl_stor:
178
      raise errors.ProgrammerError("FileStorage device requested with"
179
                                   " exclusive_storage")
180
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
181
      raise ValueError("Invalid configuration data %s" % str(unique_id))
182

    
183
    dev_path = unique_id[1]
184

    
185
    CheckFileStoragePathAcceptance(dev_path)
186

    
187
    try:
188
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
189
      f = os.fdopen(fd, "w")
190
      f.truncate(size * 1024 * 1024)
191
      f.close()
192
    except EnvironmentError, err:
193
      if err.errno == errno.EEXIST:
194
        base.ThrowError("File already existing: %s", dev_path)
195
      base.ThrowError("Error in file creation: %", str(err))
196

    
197
    return FileStorage(unique_id, children, size, params, dyn_params)
198

    
199

    
200
def GetFileStorageSpaceInfo(path):
201
  """Retrieves the free and total space of the device where the file is
202
     located.
203

204
     @type path: string
205
     @param path: Path of the file whose embracing device's capacity is
206
       reported.
207
     @return: a dictionary containing 'vg_size' and 'vg_free' given in MebiBytes
208

209
  """
210
  try:
211
    result = os.statvfs(path)
212
    free = (result.f_frsize * result.f_bavail) / (1024 * 1024)
213
    size = (result.f_frsize * result.f_blocks) / (1024 * 1024)
214
    return {"type": constants.ST_FILE,
215
            "name": path,
216
            "storage_size": size,
217
            "storage_free": free}
218
  except OSError, e:
219
    raise errors.CommandError("Failed to retrieve file system information about"
220
                              " path: %s - %s" % (path, e.strerror))
221

    
222

    
223
def _GetForbiddenFileStoragePaths():
224
  """Builds a list of path prefixes which shouldn't be used for file storage.
225

226
  @rtype: frozenset
227

228
  """
229
  paths = set([
230
    "/boot",
231
    "/dev",
232
    "/etc",
233
    "/home",
234
    "/proc",
235
    "/root",
236
    "/sys",
237
    ])
238

    
239
  for prefix in ["", "/usr", "/usr/local"]:
240
    paths.update(map(lambda s: "%s/%s" % (prefix, s),
241
                     ["bin", "lib", "lib32", "lib64", "sbin"]))
242

    
243
  return compat.UniqueFrozenset(map(os.path.normpath, paths))
244

    
245

    
246
def _ComputeWrongFileStoragePaths(paths,
247
                                  _forbidden=_GetForbiddenFileStoragePaths()):
248
  """Cross-checks a list of paths for prefixes considered bad.
249

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

252
  @type paths: list
253
  @param paths: List of paths to be checked
254
  @rtype: list
255
  @return: Sorted list of paths for which the user should be warned
256

257
  """
258
  def _Check(path):
259
    return (not os.path.isabs(path) or
260
            path in _forbidden or
261
            filter(lambda p: utils.IsBelowDir(p, path), _forbidden))
262

    
263
  return utils.NiceSort(filter(_Check, map(os.path.normpath, paths)))
264

    
265

    
266
def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE):
267
  """Returns a list of file storage paths whose prefix is considered bad.
268

269
  See L{_ComputeWrongFileStoragePaths}.
270

271
  """
272
  return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename))
273

    
274

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

278
  @type path: string
279
  @param path: Path to check
280
  @type allowed: list
281
  @param allowed: List of allowed paths
282
  @type exact_match_ok: bool
283
  @param exact_match_ok: whether or not it is okay when the path is exactly
284
      equal to an allowed path and not a subdir of it
285
  @raise errors.FileStoragePathError: If the path is not allowed
286

287
  """
288
  if not os.path.isabs(path):
289
    raise errors.FileStoragePathError("File storage path must be absolute,"
290
                                      " got '%s'" % path)
291

    
292
  for i in allowed:
293
    if not os.path.isabs(i):
294
      logging.info("Ignoring relative path '%s' for file storage", i)
295
      continue
296

    
297
    if exact_match_ok:
298
      if os.path.normpath(i) == os.path.normpath(path):
299
        break
300

    
301
    if utils.IsBelowDir(i, path):
302
      break
303
  else:
304
    raise errors.FileStoragePathError("Path '%s' is not acceptable for file"
305
                                      " storage" % path)
306

    
307

    
308
def _LoadAllowedFileStoragePaths(filename):
309
  """Loads file containing allowed file storage paths.
310

311
  @rtype: list
312
  @return: List of allowed paths (can be an empty list)
313

314
  """
315
  try:
316
    contents = utils.ReadFile(filename)
317
  except EnvironmentError:
318
    return []
319
  else:
320
    return utils.FilterEmptyLinesAndComments(contents)
321

    
322

    
323
def CheckFileStoragePathAcceptance(
324
    path, _filename=pathutils.FILE_STORAGE_PATHS_FILE,
325
    exact_match_ok=False):
326
  """Checks if a path is allowed for file storage.
327

328
  @type path: string
329
  @param path: Path to check
330
  @raise errors.FileStoragePathError: If the path is not allowed
331

332
  """
333
  allowed = _LoadAllowedFileStoragePaths(_filename)
334
  if not allowed:
335
    raise errors.FileStoragePathError("No paths are valid or path file '%s'"
336
                                      " was not accessible." % _filename)
337

    
338
  if _ComputeWrongFileStoragePaths([path]):
339
    raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" %
340
                                      path)
341

    
342
  _CheckFileStoragePath(path, allowed, exact_match_ok=exact_match_ok)
343

    
344

    
345
def _CheckFileStoragePathExistance(path):
346
  """Checks whether the given path is usable on the file system.
347

348
  This checks wether the path is existing, a directory and writable.
349

350
  @type path: string
351
  @param path: path to check
352

353
  """
354
  if not os.path.isdir(path):
355
    raise errors.FileStoragePathError("Path '%s' is not existing or not a"
356
                                      " directory." % path)
357
  if not os.access(path, os.W_OK):
358
    raise errors.FileStoragePathError("Path '%s' is not writable" % path)
359

    
360

    
361
def CheckFileStoragePath(
362
    path, _allowed_paths_file=pathutils.FILE_STORAGE_PATHS_FILE):
363
  """Checks whether the path exists and is acceptable to use.
364

365
  Can be used for any file-based storage, for example shared-file storage.
366

367
  @type path: string
368
  @param path: path to check
369
  @rtype: string
370
  @returns: error message if the path is not ready to use
371

372
  """
373
  try:
374
    CheckFileStoragePathAcceptance(path, _filename=_allowed_paths_file,
375
                                   exact_match_ok=True)
376
  except errors.FileStoragePathError as e:
377
    return str(e)
378
  if not os.path.isdir(path):
379
    return "Path '%s' is not exisiting or not a directory." % path
380
  if not os.access(path, os.W_OK):
381
    return "Path '%s' is not writable" % path