root / lib / storage / filestorage.py @ 267520ba
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): |
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) |
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 |