1 |
|
#
|
2 |
|
#
|
3 |
|
|
4 |
|
# Copyright (C) 2006, 2007, 2010, 2011, 2012, 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 |
|
"""Block device abstraction"""
|
23 |
|
|
24 |
|
import re
|
25 |
|
import errno
|
26 |
|
import stat
|
27 |
|
import os
|
28 |
|
import logging
|
29 |
|
import math
|
30 |
|
|
31 |
|
from ganeti import utils
|
32 |
|
from ganeti import errors
|
33 |
|
from ganeti import constants
|
34 |
|
from ganeti import objects
|
35 |
|
from ganeti import compat
|
36 |
|
from ganeti import pathutils
|
37 |
|
from ganeti import serializer
|
38 |
|
from ganeti.block import drbd
|
39 |
|
from ganeti.block import base
|
40 |
|
|
41 |
|
|
42 |
|
class RbdShowmappedJsonError(Exception):
|
43 |
|
"""`rbd showmmapped' JSON formatting error Exception class.
|
44 |
|
|
45 |
|
"""
|
46 |
|
pass
|
47 |
|
|
48 |
|
|
49 |
|
def _CheckResult(result):
|
50 |
|
"""Throws an error if the given result is a failed one.
|
51 |
|
|
52 |
|
@param result: result from RunCmd
|
53 |
|
|
54 |
|
"""
|
55 |
|
if result.failed:
|
56 |
|
base.ThrowError("Command: %s error: %s - %s",
|
57 |
|
result.cmd, result.fail_reason, result.output)
|
58 |
|
|
59 |
|
|
60 |
|
def _GetForbiddenFileStoragePaths():
|
61 |
|
"""Builds a list of path prefixes which shouldn't be used for file storage.
|
62 |
|
|
63 |
|
@rtype: frozenset
|
64 |
|
|
65 |
|
"""
|
66 |
|
paths = set([
|
67 |
|
"/boot",
|
68 |
|
"/dev",
|
69 |
|
"/etc",
|
70 |
|
"/home",
|
71 |
|
"/proc",
|
72 |
|
"/root",
|
73 |
|
"/sys",
|
74 |
|
])
|
75 |
|
|
76 |
|
for prefix in ["", "/usr", "/usr/local"]:
|
77 |
|
paths.update(map(lambda s: "%s/%s" % (prefix, s),
|
78 |
|
["bin", "lib", "lib32", "lib64", "sbin"]))
|
79 |
|
|
80 |
|
return compat.UniqueFrozenset(map(os.path.normpath, paths))
|
81 |
|
|
82 |
|
|
83 |
|
def _ComputeWrongFileStoragePaths(paths,
|
84 |
|
_forbidden=_GetForbiddenFileStoragePaths()):
|
85 |
|
"""Cross-checks a list of paths for prefixes considered bad.
|
86 |
|
|
87 |
|
Some paths, e.g. "/bin", should not be used for file storage.
|
88 |
|
|
89 |
|
@type paths: list
|
90 |
|
@param paths: List of paths to be checked
|
91 |
|
@rtype: list
|
92 |
|
@return: Sorted list of paths for which the user should be warned
|
93 |
|
|
94 |
|
"""
|
95 |
|
def _Check(path):
|
96 |
|
return (not os.path.isabs(path) or
|
97 |
|
path in _forbidden or
|
98 |
|
filter(lambda p: utils.IsBelowDir(p, path), _forbidden))
|
99 |
|
|
100 |
|
return utils.NiceSort(filter(_Check, map(os.path.normpath, paths)))
|
101 |
|
|
102 |
|
|
103 |
|
def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE):
|
104 |
|
"""Returns a list of file storage paths whose prefix is considered bad.
|
105 |
|
|
106 |
|
See L{_ComputeWrongFileStoragePaths}.
|
107 |
|
|
108 |
|
"""
|
109 |
|
return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename))
|
110 |
|
|
111 |
|
|
112 |
|
def _CheckFileStoragePath(path, allowed):
|
113 |
|
"""Checks if a path is in a list of allowed paths for file storage.
|
114 |
|
|
115 |
|
@type path: string
|
116 |
|
@param path: Path to check
|
117 |
|
@type allowed: list
|
118 |
|
@param allowed: List of allowed paths
|
119 |
|
@raise errors.FileStoragePathError: If the path is not allowed
|
120 |
|
|
121 |
|
"""
|
122 |
|
if not os.path.isabs(path):
|
123 |
|
raise errors.FileStoragePathError("File storage path must be absolute,"
|
124 |
|
" got '%s'" % path)
|
125 |
|
|
126 |
|
for i in allowed:
|
127 |
|
if not os.path.isabs(i):
|
128 |
|
logging.info("Ignoring relative path '%s' for file storage", i)
|
129 |
|
continue
|
130 |
|
|
131 |
|
if utils.IsBelowDir(i, path):
|
132 |
|
break
|
133 |
|
else:
|
134 |
|
raise errors.FileStoragePathError("Path '%s' is not acceptable for file"
|
135 |
|
" storage" % path)
|
136 |
|
|
137 |
|
|
138 |
|
def _LoadAllowedFileStoragePaths(filename):
|
139 |
|
"""Loads file containing allowed file storage paths.
|
140 |
|
|
141 |
|
@rtype: list
|
142 |
|
@return: List of allowed paths (can be an empty list)
|
143 |
|
|
144 |
|
"""
|
145 |
|
try:
|
146 |
|
contents = utils.ReadFile(filename)
|
147 |
|
except EnvironmentError:
|
148 |
|
return []
|
149 |
|
else:
|
150 |
|
return utils.FilterEmptyLinesAndComments(contents)
|
151 |
|
|
152 |
|
|
153 |
|
def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE):
|
154 |
|
"""Checks if a path is allowed for file storage.
|
155 |
|
|
156 |
|
@type path: string
|
157 |
|
@param path: Path to check
|
158 |
|
@raise errors.FileStoragePathError: If the path is not allowed
|
159 |
|
|
160 |
|
"""
|
161 |
|
allowed = _LoadAllowedFileStoragePaths(_filename)
|
162 |
|
|
163 |
|
if _ComputeWrongFileStoragePaths([path]):
|
164 |
|
raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" %
|
165 |
|
path)
|
166 |
|
|
167 |
|
_CheckFileStoragePath(path, allowed)
|
168 |
|
|
169 |
|
|
170 |
|
class LogicalVolume(base.BlockDev):
|
171 |
|
"""Logical Volume block device.
|
172 |
|
|
173 |
|
"""
|
174 |
|
_VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
|
175 |
|
_INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"])
|
176 |
|
_INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"])
|
177 |
|
|
178 |
|
def __init__(self, unique_id, children, size, params):
|
179 |
|
"""Attaches to a LV device.
|
180 |
|
|
181 |
|
The unique_id is a tuple (vg_name, lv_name)
|
182 |
|
|
183 |
|
"""
|
184 |
|
super(LogicalVolume, self).__init__(unique_id, children, size, params)
|
185 |
|
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
|
186 |
|
raise ValueError("Invalid configuration data %s" % str(unique_id))
|
187 |
|
self._vg_name, self._lv_name = unique_id
|
188 |
|
self._ValidateName(self._vg_name)
|
189 |
|
self._ValidateName(self._lv_name)
|
190 |
|
self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
|
191 |
|
self._degraded = True
|
192 |
|
self.major = self.minor = self.pe_size = self.stripe_count = None
|
193 |
|
self.Attach()
|
194 |
|
|
195 |
|
@staticmethod
|
196 |
|
def _GetStdPvSize(pvs_info):
|
197 |
|
"""Return the the standard PV size (used with exclusive storage).
|
198 |
|
|
199 |
|
@param pvs_info: list of objects.LvmPvInfo, cannot be empty
|
200 |
|
@rtype: float
|
201 |
|
@return: size in MiB
|
202 |
|
|
203 |
|
"""
|
204 |
|
assert len(pvs_info) > 0
|
205 |
|
smallest = min([pv.size for pv in pvs_info])
|
206 |
|
return smallest / (1 + constants.PART_MARGIN + constants.PART_RESERVED)
|
207 |
|
|
208 |
|
@staticmethod
|
209 |
|
def _ComputeNumPvs(size, pvs_info):
|
210 |
|
"""Compute the number of PVs needed for an LV (with exclusive storage).
|
211 |
|
|
212 |
|
@type size: float
|
213 |
|
@param size: LV size in MiB
|
214 |
|
@param pvs_info: list of objects.LvmPvInfo, cannot be empty
|
215 |
|
@rtype: integer
|
216 |
|
@return: number of PVs needed
|
217 |
|
"""
|
218 |
|
assert len(pvs_info) > 0
|
219 |
|
pv_size = float(LogicalVolume._GetStdPvSize(pvs_info))
|
220 |
|
return int(math.ceil(float(size) / pv_size))
|
221 |
|
|
222 |
|
@staticmethod
|
223 |
|
def _GetEmptyPvNames(pvs_info, max_pvs=None):
|
224 |
|
"""Return a list of empty PVs, by name.
|
225 |
|
|
226 |
|
"""
|
227 |
|
empty_pvs = filter(objects.LvmPvInfo.IsEmpty, pvs_info)
|
228 |
|
if max_pvs is not None:
|
229 |
|
empty_pvs = empty_pvs[:max_pvs]
|
230 |
|
return map((lambda pv: pv.name), empty_pvs)
|
231 |
|
|
232 |
|
@classmethod
|
233 |
|
def Create(cls, unique_id, children, size, params, excl_stor):
|
234 |
|
"""Create a new logical volume.
|
235 |
|
|
236 |
|
"""
|
237 |
|
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
|
238 |
|
raise errors.ProgrammerError("Invalid configuration data %s" %
|
239 |
|
str(unique_id))
|
240 |
|
vg_name, lv_name = unique_id
|
241 |
|
cls._ValidateName(vg_name)
|
242 |
|
cls._ValidateName(lv_name)
|
243 |
|
pvs_info = cls.GetPVInfo([vg_name])
|
244 |
|
if not pvs_info:
|
245 |
|
if excl_stor:
|
246 |
|
msg = "No (empty) PVs found"
|
247 |
|
else:
|
248 |
|
msg = "Can't compute PV info for vg %s" % vg_name
|
249 |
|
base.ThrowError(msg)
|
250 |
|
pvs_info.sort(key=(lambda pv: pv.free), reverse=True)
|
251 |
|
|
252 |
|
pvlist = [pv.name for pv in pvs_info]
|
253 |
|
if compat.any(":" in v for v in pvlist):
|
254 |
|
base.ThrowError("Some of your PVs have the invalid character ':' in their"
|
255 |
|
" name, this is not supported - please filter them out"
|
256 |
|
" in lvm.conf using either 'filter' or 'preferred_names'")
|
257 |
|
|
258 |
|
current_pvs = len(pvlist)
|
259 |
|
desired_stripes = params[constants.LDP_STRIPES]
|
260 |
|
stripes = min(current_pvs, desired_stripes)
|
261 |
|
|
262 |
|
if excl_stor:
|
263 |
|
(err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info)
|
264 |
|
if err_msgs:
|
265 |
|
for m in err_msgs:
|
266 |
|
logging.warning(m)
|
267 |
|
req_pvs = cls._ComputeNumPvs(size, pvs_info)
|
268 |
|
pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs)
|
269 |
|
current_pvs = len(pvlist)
|
270 |
|
if current_pvs < req_pvs:
|
271 |
|
base.ThrowError("Not enough empty PVs to create a disk of %d MB:"
|
272 |
|
" %d available, %d needed", size, current_pvs, req_pvs)
|
273 |
|
assert current_pvs == len(pvlist)
|
274 |
|
if stripes > current_pvs:
|
275 |
|
# No warning issued for this, as it's no surprise
|
276 |
|
stripes = current_pvs
|
277 |
|
|
278 |
|
else:
|
279 |
|
if stripes < desired_stripes:
|
280 |
|
logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
|
281 |
|
" available.", desired_stripes, vg_name, current_pvs)
|
282 |
|
free_size = sum([pv.free for pv in pvs_info])
|
283 |
|
# The size constraint should have been checked from the master before
|
284 |
|
# calling the create function.
|
285 |
|
if free_size < size:
|
286 |
|
base.ThrowError("Not enough free space: required %s,"
|
287 |
|
" available %s", size, free_size)
|
288 |
|
|
289 |
|
# If the free space is not well distributed, we won't be able to
|
290 |
|
# create an optimally-striped volume; in that case, we want to try
|
291 |
|
# with N, N-1, ..., 2, and finally 1 (non-stripped) number of
|
292 |
|
# stripes
|
293 |
|
cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
|
294 |
|
for stripes_arg in range(stripes, 0, -1):
|
295 |
|
result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
|
296 |
|
if not result.failed:
|
297 |
|
break
|
298 |
|
if result.failed:
|
299 |
|
base.ThrowError("LV create failed (%s): %s",
|
300 |
|
result.fail_reason, result.output)
|
301 |
|
return LogicalVolume(unique_id, children, size, params)
|
302 |
|
|
303 |
|
@staticmethod
|
304 |
|
def _GetVolumeInfo(lvm_cmd, fields):
|
305 |
|
"""Returns LVM Volume infos using lvm_cmd
|
306 |
|
|
307 |
|
@param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
|
308 |
|
@param fields: Fields to return
|
309 |
|
@return: A list of dicts each with the parsed fields
|
310 |
|
|
311 |
|
"""
|
312 |
|
if not fields:
|
313 |
|
raise errors.ProgrammerError("No fields specified")
|
314 |
|
|
315 |
|
sep = "|"
|
316 |
|
cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
|
317 |
|
"--separator=%s" % sep, "-o%s" % ",".join(fields)]
|
318 |
|
|
319 |
|
result = utils.RunCmd(cmd)
|
320 |
|
if result.failed:
|
321 |
|
raise errors.CommandError("Can't get the volume information: %s - %s" %
|
322 |
|
(result.fail_reason, result.output))
|
323 |
|
|
324 |
|
data = []
|
325 |
|
for line in result.stdout.splitlines():
|
326 |
|
splitted_fields = line.strip().split(sep)
|
327 |
|
|
328 |
|
if len(fields) != len(splitted_fields):
|
329 |
|
raise errors.CommandError("Can't parse %s output: line '%s'" %
|
330 |
|
(lvm_cmd, line))
|
331 |
|
|
332 |
|
data.append(splitted_fields)
|
333 |
|
|
334 |
|
return data
|
335 |
|
|
336 |
|
@classmethod
|
337 |
|
def GetPVInfo(cls, vg_names, filter_allocatable=True, include_lvs=False):
|
338 |
|
"""Get the free space info for PVs in a volume group.
|
339 |
|
|
340 |
|
@param vg_names: list of volume group names, if empty all will be returned
|
341 |
|
@param filter_allocatable: whether to skip over unallocatable PVs
|
342 |
|
@param include_lvs: whether to include a list of LVs hosted on each PV
|
343 |
|
|
344 |
|
@rtype: list
|
345 |
|
@return: list of objects.LvmPvInfo objects
|
346 |
|
|
347 |
|
"""
|
348 |
|
# We request "lv_name" field only if we care about LVs, so we don't get
|
349 |
|
# a long list of entries with many duplicates unless we really have to.
|
350 |
|
# The duplicate "pv_name" field will be ignored.
|
351 |
|
if include_lvs:
|
352 |
|
lvfield = "lv_name"
|
353 |
|
else:
|
354 |
|
lvfield = "pv_name"
|
355 |
|
try:
|
356 |
|
info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
|
357 |
|
"pv_attr", "pv_size", lvfield])
|
358 |
|
except errors.GenericError, err:
|
359 |
|
logging.error("Can't get PV information: %s", err)
|
360 |
|
return None
|
361 |
|
|
362 |
|
# When asked for LVs, "pvs" may return multiple entries for the same PV-LV
|
363 |
|
# pair. We sort entries by PV name and then LV name, so it's easy to weed
|
364 |
|
# out duplicates.
|
365 |
|
if include_lvs:
|
366 |
|
info.sort(key=(lambda i: (i[0], i[5])))
|
367 |
|
data = []
|
368 |
|
lastpvi = None
|
369 |
|
for (pv_name, vg_name, pv_free, pv_attr, pv_size, lv_name) in info:
|
370 |
|
# (possibly) skip over pvs which are not allocatable
|
371 |
|
if filter_allocatable and pv_attr[0] != "a":
|
372 |
|
continue
|
373 |
|
# (possibly) skip over pvs which are not in the right volume group(s)
|
374 |
|
if vg_names and vg_name not in vg_names:
|
375 |
|
continue
|
376 |
|
# Beware of duplicates (check before inserting)
|
377 |
|
if lastpvi and lastpvi.name == pv_name:
|
378 |
|
if include_lvs and lv_name:
|
379 |
|
if not lastpvi.lv_list or lastpvi.lv_list[-1] != lv_name:
|
380 |
|
lastpvi.lv_list.append(lv_name)
|
381 |
|
else:
|
382 |
|
if include_lvs and lv_name:
|
383 |
|
lvl = [lv_name]
|
384 |
|
else:
|
385 |
|
lvl = []
|
386 |
|
lastpvi = objects.LvmPvInfo(name=pv_name, vg_name=vg_name,
|
387 |
|
size=float(pv_size), free=float(pv_free),
|
388 |
|
attributes=pv_attr, lv_list=lvl)
|
389 |
|
data.append(lastpvi)
|
390 |
|
|
391 |
|
return data
|
392 |
|
|
393 |
|
@classmethod
|
394 |
|
def _GetExclusiveStorageVgFree(cls, vg_name):
|
395 |
|
"""Return the free disk space in the given VG, in exclusive storage mode.
|
396 |
|
|
397 |
|
@type vg_name: string
|
398 |
|
@param vg_name: VG name
|
399 |
|
@rtype: float
|
400 |
|
@return: free space in MiB
|
401 |
|
"""
|
402 |
|
pvs_info = cls.GetPVInfo([vg_name])
|
403 |
|
if not pvs_info:
|
404 |
|
return 0.0
|
405 |
|
pv_size = cls._GetStdPvSize(pvs_info)
|
406 |
|
num_pvs = len(cls._GetEmptyPvNames(pvs_info))
|
407 |
|
return pv_size * num_pvs
|
408 |
|
|
409 |
|
@classmethod
|
410 |
|
def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
|
411 |
|
"""Get the free space info for specific VGs.
|
412 |
|
|
413 |
|
@param vg_names: list of volume group names, if empty all will be returned
|
414 |
|
@param excl_stor: whether exclusive_storage is enabled
|
415 |
|
@param filter_readonly: whether to skip over readonly VGs
|
416 |
|
|
417 |
|
@rtype: list
|
418 |
|
@return: list of tuples (free_space, total_size, name) with free_space in
|
419 |
|
MiB
|
420 |
|
|
421 |
|
"""
|
422 |
|
try:
|
423 |
|
info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
|
424 |
|
"vg_size"])
|
425 |
|
except errors.GenericError, err:
|
426 |
|
logging.error("Can't get VG information: %s", err)
|
427 |
|
return None
|
428 |
|
|
429 |
|
data = []
|
430 |
|
for vg_name, vg_free, vg_attr, vg_size in info:
|
431 |
|
# (possibly) skip over vgs which are not writable
|
432 |
|
if filter_readonly and vg_attr[0] == "r":
|
433 |
|
continue
|
434 |
|
# (possibly) skip over vgs which are not in the right volume group(s)
|
435 |
|
if vg_names and vg_name not in vg_names:
|
436 |
|
continue
|
437 |
|
# Exclusive storage needs a different concept of free space
|
438 |
|
if excl_stor:
|
439 |
|
es_free = cls._GetExclusiveStorageVgFree(vg_name)
|
440 |
|
assert es_free <= vg_free
|
441 |
|
vg_free = es_free
|
442 |
|
data.append((float(vg_free), float(vg_size), vg_name))
|
443 |
|
|
444 |
|
return data
|
445 |
|
|
446 |
|
@classmethod
|
447 |
|
def _ValidateName(cls, name):
|
448 |
|
"""Validates that a given name is valid as VG or LV name.
|
449 |
|
|
450 |
|
The list of valid characters and restricted names is taken out of
|
451 |
|
the lvm(8) manpage, with the simplification that we enforce both
|
452 |
|
VG and LV restrictions on the names.
|
453 |
|
|
454 |
|
"""
|
455 |
|
if (not cls._VALID_NAME_RE.match(name) or
|
456 |
|
name in cls._INVALID_NAMES or
|
457 |
|
compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
|
458 |
|
base.ThrowError("Invalid LVM name '%s'", name)
|
459 |
|
|
460 |
|
def Remove(self):
|
461 |
|
"""Remove this logical volume.
|
462 |
|
|
463 |
|
"""
|
464 |
|
if not self.minor and not self.Attach():
|
465 |
|
# the LV does not exist
|
466 |
|
return
|
467 |
|
result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
|
468 |
|
(self._vg_name, self._lv_name)])
|
469 |
|
if result.failed:
|
470 |
|
base.ThrowError("Can't lvremove: %s - %s",
|
471 |
|
result.fail_reason, result.output)
|
472 |
|
|
473 |
|
def Rename(self, new_id):
|
474 |
|
"""Rename this logical volume.
|
475 |
|
|
476 |
|
"""
|
477 |
|
if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
|
478 |
|
raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
|
479 |
|
new_vg, new_name = new_id
|
480 |
|
if new_vg != self._vg_name:
|
481 |
|
raise errors.ProgrammerError("Can't move a logical volume across"
|
482 |
|
" volume groups (from %s to to %s)" %
|
483 |
|
(self._vg_name, new_vg))
|
484 |
|
result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
|
485 |
|
if result.failed:
|
486 |
|
base.ThrowError("Failed to rename the logical volume: %s", result.output)
|
487 |
|
self._lv_name = new_name
|
488 |
|
self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
|
489 |
|
|
490 |
|
def Attach(self):
|
491 |
|
"""Attach to an existing LV.
|
492 |
|
|
493 |
|
This method will try to see if an existing and active LV exists
|
494 |
|
which matches our name. If so, its major/minor will be
|
495 |
|
recorded.
|
496 |
|
|
497 |
|
"""
|
498 |
|
self.attached = False
|
499 |
|
result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
|
500 |
|
"--units=k", "--nosuffix",
|
501 |
|
"-olv_attr,lv_kernel_major,lv_kernel_minor,"
|
502 |
|
"vg_extent_size,stripes", self.dev_path])
|
503 |
|
if result.failed:
|
504 |
|
logging.error("Can't find LV %s: %s, %s",
|
505 |
|
self.dev_path, result.fail_reason, result.output)
|
506 |
|
return False
|
507 |
|
# the output can (and will) have multiple lines for multi-segment
|
508 |
|
# LVs, as the 'stripes' parameter is a segment one, so we take
|
509 |
|
# only the last entry, which is the one we're interested in; note
|
510 |
|
# that with LVM2 anyway the 'stripes' value must be constant
|
511 |
|
# across segments, so this is a no-op actually
|
512 |
|
out = result.stdout.splitlines()
|
513 |
|
if not out: # totally empty result? splitlines() returns at least
|
514 |
|
# one line for any non-empty string
|
515 |
|
logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
|
516 |
|
return False
|
517 |
|
out = out[-1].strip().rstrip(",")
|
518 |
|
out = out.split(",")
|
519 |
|
if len(out) != 5:
|
520 |
|
logging.error("Can't parse LVS output, len(%s) != 5", str(out))
|
521 |
|
return False
|
522 |
|
|
523 |
|
status, major, minor, pe_size, stripes = out
|
524 |
|
if len(status) < 6:
|
525 |
|
logging.error("lvs lv_attr is not at least 6 characters (%s)", status)
|
526 |
|
return False
|
527 |
|
|
528 |
|
try:
|
529 |
|
major = int(major)
|
530 |
|
minor = int(minor)
|
531 |
|
except (TypeError, ValueError), err:
|
532 |
|
logging.error("lvs major/minor cannot be parsed: %s", str(err))
|
533 |
|
|
534 |
|
try:
|
535 |
|
pe_size = int(float(pe_size))
|
536 |
|
except (TypeError, ValueError), err:
|
537 |
|
logging.error("Can't parse vg extent size: %s", err)
|
538 |
|
return False
|
539 |
|
|
540 |
|
try:
|
541 |
|
stripes = int(stripes)
|
542 |
|
except (TypeError, ValueError), err:
|
543 |
|
logging.error("Can't parse the number of stripes: %s", err)
|
544 |
|
return False
|
545 |
|
|
546 |
|
self.major = major
|
547 |
|
self.minor = minor
|
548 |
|
self.pe_size = pe_size
|
549 |
|
self.stripe_count = stripes
|
550 |
|
self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
|
551 |
|
# storage
|
552 |
|
self.attached = True
|
553 |
|
return True
|
554 |
|
|
555 |
|
def Assemble(self):
|
556 |
|
"""Assemble the device.
|
557 |
|
|
558 |
|
We always run `lvchange -ay` on the LV to ensure it's active before
|
559 |
|
use, as there were cases when xenvg was not active after boot
|
560 |
|
(also possibly after disk issues).
|
561 |
|
|
562 |
|
"""
|
563 |
|
result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
|
564 |
|
if result.failed:
|
565 |
|
base.ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
|
566 |
|
|
567 |
|
def Shutdown(self):
|
568 |
|
"""Shutdown the device.
|
569 |
|
|
570 |
|
This is a no-op for the LV device type, as we don't deactivate the
|
571 |
|
volumes on shutdown.
|
572 |
|
|
573 |
|
"""
|
574 |
|
pass
|
575 |
|
|
576 |
|
def GetSyncStatus(self):
|
577 |
|
"""Returns the sync status of the device.
|
578 |
|
|
579 |
|
If this device is a mirroring device, this function returns the
|
580 |
|
status of the mirror.
|
581 |
|
|
582 |
|
For logical volumes, sync_percent and estimated_time are always
|
583 |
|
None (no recovery in progress, as we don't handle the mirrored LV
|
584 |
|
case). The is_degraded parameter is the inverse of the ldisk
|
585 |
|
parameter.
|
586 |
|
|
587 |
|
For the ldisk parameter, we check if the logical volume has the
|
588 |
|
'virtual' type, which means it's not backed by existing storage
|
589 |
|
anymore (read from it return I/O error). This happens after a
|
590 |
|
physical disk failure and subsequent 'vgreduce --removemissing' on
|
591 |
|
the volume group.
|
592 |
|
|
593 |
|
The status was already read in Attach, so we just return it.
|
594 |
|
|
595 |
|
@rtype: objects.BlockDevStatus
|
596 |
|
|
597 |
|
"""
|
598 |
|
if self._degraded:
|
599 |
|
ldisk_status = constants.LDS_FAULTY
|
600 |
|
else:
|
601 |
|
ldisk_status = constants.LDS_OKAY
|
602 |
|
|
603 |
|
return objects.BlockDevStatus(dev_path=self.dev_path,
|
604 |
|
major=self.major,
|
605 |
|
minor=self.minor,
|
606 |
|
sync_percent=None,
|
607 |
|
estimated_time=None,
|
608 |
|
is_degraded=self._degraded,
|
609 |
|
ldisk_status=ldisk_status)
|
610 |
|
|
611 |
|
def Open(self, force=False):
|
612 |
|
"""Make the device ready for I/O.
|
613 |
|
|
614 |
|
This is a no-op for the LV device type.
|
615 |
|
|
616 |
|
"""
|
617 |
|
pass
|
618 |
|
|
619 |
|
def Close(self):
|
620 |
|
"""Notifies that the device will no longer be used for I/O.
|
621 |
|
|
622 |
|
This is a no-op for the LV device type.
|
623 |
|
|
624 |
|
"""
|
625 |
|
pass
|
626 |
|
|
627 |
|
def Snapshot(self, size):
|
628 |
|
"""Create a snapshot copy of an lvm block device.
|
629 |
|
|
630 |
|
@returns: tuple (vg, lv)
|
631 |
|
|
632 |
|
"""
|
633 |
|
snap_name = self._lv_name + ".snap"
|
634 |
|
|
635 |
|
# remove existing snapshot if found
|
636 |
|
snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
|
637 |
|
base.IgnoreError(snap.Remove)
|
638 |
|
|
639 |
|
vg_info = self.GetVGInfo([self._vg_name], False)
|
640 |
|
if not vg_info:
|
641 |
|
base.ThrowError("Can't compute VG info for vg %s", self._vg_name)
|
642 |
|
free_size, _, _ = vg_info[0]
|
643 |
|
if free_size < size:
|
644 |
|
base.ThrowError("Not enough free space: required %s,"
|
645 |
|
" available %s", size, free_size)
|
646 |
|
|
647 |
|
_CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
|
648 |
|
"-n%s" % snap_name, self.dev_path]))
|
649 |
|
|
650 |
|
return (self._vg_name, snap_name)
|
651 |
|
|
652 |
|
def _RemoveOldInfo(self):
|
653 |
|
"""Try to remove old tags from the lv.
|
654 |
|
|
655 |
|
"""
|
656 |
|
result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
|
657 |
|
self.dev_path])
|
658 |
|
_CheckResult(result)
|
659 |
|
|
660 |
|
raw_tags = result.stdout.strip()
|
661 |
|
if raw_tags:
|
662 |
|
for tag in raw_tags.split(","):
|
663 |
|
_CheckResult(utils.RunCmd(["lvchange", "--deltag",
|
664 |
|
tag.strip(), self.dev_path]))
|
665 |
|
|
666 |
|
def SetInfo(self, text):
|
667 |
|
"""Update metadata with info text.
|
668 |
|
|
669 |
|
"""
|
670 |
|
base.BlockDev.SetInfo(self, text)
|
671 |
|
|
672 |
|
self._RemoveOldInfo()
|
673 |
|
|
674 |
|
# Replace invalid characters
|
675 |
|
text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
|
676 |
|
text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
|
677 |
|
|
678 |
|
# Only up to 128 characters are allowed
|
679 |
|
text = text[:128]
|
680 |
|
|
681 |
|
_CheckResult(utils.RunCmd(["lvchange", "--addtag", text, self.dev_path]))
|
682 |
|
|
683 |
|
def Grow(self, amount, dryrun, backingstore):
|
684 |
|
"""Grow the logical volume.
|
685 |
|
|
686 |
|
"""
|
687 |
|
if not backingstore:
|
688 |
|
return
|
689 |
|
if self.pe_size is None or self.stripe_count is None:
|
690 |
|
if not self.Attach():
|
691 |
|
base.ThrowError("Can't attach to LV during Grow()")
|
692 |
|
full_stripe_size = self.pe_size * self.stripe_count
|
693 |
|
# pe_size is in KB
|
694 |
|
amount *= 1024
|
695 |
|
rest = amount % full_stripe_size
|
696 |
|
if rest != 0:
|
697 |
|
amount += full_stripe_size - rest
|
698 |
|
cmd = ["lvextend", "-L", "+%dk" % amount]
|
699 |
|
if dryrun:
|
700 |
|
cmd.append("--test")
|
701 |
|
# we try multiple algorithms since the 'best' ones might not have
|
702 |
|
# space available in the right place, but later ones might (since
|
703 |
|
# they have less constraints); also note that only recent LVM
|
704 |
|
# supports 'cling'
|
705 |
|
for alloc_policy in "contiguous", "cling", "normal":
|
706 |
|
result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
|
707 |
|
if not result.failed:
|
708 |
|
return
|
709 |
|
base.ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
|
710 |
|
|
711 |
|
|
712 |
|
class FileStorage(base.BlockDev):
|
713 |
|
"""File device.
|
714 |
|
|
715 |
|
This class represents the a file storage backend device.
|
716 |
|
|
717 |
|
The unique_id for the file device is a (file_driver, file_path) tuple.
|
718 |
|
|
719 |
|
"""
|
720 |
|
def __init__(self, unique_id, children, size, params):
|
721 |
|
"""Initalizes a file device backend.
|
722 |
|
|
723 |
|
"""
|
724 |
|
if children:
|
725 |
|
raise errors.BlockDeviceError("Invalid setup for file device")
|
726 |
|
super(FileStorage, self).__init__(unique_id, children, size, params)
|
727 |
|
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
|
728 |
|
raise ValueError("Invalid configuration data %s" % str(unique_id))
|
729 |
|
self.driver = unique_id[0]
|
730 |
|
self.dev_path = unique_id[1]
|
731 |
|
|
732 |
|
CheckFileStoragePath(self.dev_path)
|
733 |
|
|
734 |
|
self.Attach()
|
735 |
|
|
736 |
|
def Assemble(self):
|
737 |
|
"""Assemble the device.
|
738 |
|
|
739 |
|
Checks whether the file device exists, raises BlockDeviceError otherwise.
|
740 |
|
|
741 |
|
"""
|
742 |
|
if not os.path.exists(self.dev_path):
|
743 |
|
base.ThrowError("File device '%s' does not exist" % self.dev_path)
|
744 |
|
|
745 |
|
def Shutdown(self):
|
746 |
|
"""Shutdown the device.
|
747 |
|
|
748 |
|
This is a no-op for the file type, as we don't deactivate
|
749 |
|
the file on shutdown.
|
750 |
|
|
751 |
|
"""
|
752 |
|
pass
|
753 |
|
|
754 |
|
def Open(self, force=False):
|
755 |
|
"""Make the device ready for I/O.
|
756 |
|
|
757 |
|
This is a no-op for the file type.
|
758 |
|
|
759 |
|
"""
|
760 |
|
pass
|
761 |
|
|
762 |
|
def Close(self):
|
763 |
|
"""Notifies that the device will no longer be used for I/O.
|
764 |
|
|
765 |
|
This is a no-op for the file type.
|
766 |
|
|
767 |
|
"""
|
768 |
|
pass
|
769 |
|
|
770 |
|
def Remove(self):
|
771 |
|
"""Remove the file backing the block device.
|
772 |
|
|
773 |
|
@rtype: boolean
|
774 |
|
@return: True if the removal was successful
|
775 |
|
|
776 |
|
"""
|
777 |
|
try:
|
778 |
|
os.remove(self.dev_path)
|
779 |
|
except OSError, err:
|
780 |
|
if err.errno != errno.ENOENT:
|
781 |
|
base.ThrowError("Can't remove file '%s': %s", self.dev_path, err)
|
782 |
|
|
783 |
|
def Rename(self, new_id):
|
784 |
|
"""Renames the file.
|
785 |
|
|
786 |
|
"""
|
787 |
|
# TODO: implement rename for file-based storage
|
788 |
|
base.ThrowError("Rename is not supported for file-based storage")
|
789 |
|
|
790 |
|
def Grow(self, amount, dryrun, backingstore):
|
791 |
|
"""Grow the file
|
792 |
|
|
793 |
|
@param amount: the amount (in mebibytes) to grow with
|
794 |
|
|
795 |
|
"""
|
796 |
|
if not backingstore:
|
797 |
|
return
|
798 |
|
# Check that the file exists
|
799 |
|
self.Assemble()
|
800 |
|
current_size = self.GetActualSize()
|
801 |
|
new_size = current_size + amount * 1024 * 1024
|
802 |
|
assert new_size > current_size, "Cannot Grow with a negative amount"
|
803 |
|
# We can't really simulate the growth
|
804 |
|
if dryrun:
|
805 |
|
return
|
806 |
|
try:
|
807 |
|
f = open(self.dev_path, "a+")
|
808 |
|
f.truncate(new_size)
|
809 |
|
f.close()
|
810 |
|
except EnvironmentError, err:
|
811 |
|
base.ThrowError("Error in file growth: %", str(err))
|
812 |
|
|
813 |
|
def Attach(self):
|
814 |
|
"""Attach to an existing file.
|
815 |
|
|
816 |
|
Check if this file already exists.
|
817 |
|
|
818 |
|
@rtype: boolean
|
819 |
|
@return: True if file exists
|
820 |
|
|
821 |
|
"""
|
822 |
|
self.attached = os.path.exists(self.dev_path)
|
823 |
|
return self.attached
|
824 |
|
|
825 |
|
def GetActualSize(self):
|
826 |
|
"""Return the actual disk size.
|
827 |
|
|
828 |
|
@note: the device needs to be active when this is called
|
829 |
|
|
830 |
|
"""
|
831 |
|
assert self.attached, "BlockDevice not attached in GetActualSize()"
|
832 |
|
try:
|
833 |
|
st = os.stat(self.dev_path)
|
834 |
|
return st.st_size
|
835 |
|
except OSError, err:
|
836 |
|
base.ThrowError("Can't stat %s: %s", self.dev_path, err)
|
837 |
|
|
838 |
|
@classmethod
|
839 |
|
def Create(cls, unique_id, children, size, params, excl_stor):
|
840 |
|
"""Create a new file.
|
841 |
|
|
842 |
|
@param size: the size of file in MiB
|
843 |
|
|
844 |
|
@rtype: L{bdev.FileStorage}
|
845 |
|
@return: an instance of FileStorage
|
846 |
|
|
847 |
|
"""
|
848 |
|
if excl_stor:
|
849 |
|
raise errors.ProgrammerError("FileStorage device requested with"
|
850 |
|
" exclusive_storage")
|
851 |
|
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
|
852 |
|
raise ValueError("Invalid configuration data %s" % str(unique_id))
|
853 |
|
|
854 |
|
dev_path = unique_id[1]
|
855 |
|
|
856 |
|
CheckFileStoragePath(dev_path)
|
857 |
|
|
858 |
|
try:
|
859 |
|
fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
|
860 |
|
f = os.fdopen(fd, "w")
|
861 |
|
f.truncate(size * 1024 * 1024)
|
862 |
|
f.close()
|
863 |
|
except EnvironmentError, err:
|
864 |
|
if err.errno == errno.EEXIST:
|
865 |
|
base.ThrowError("File already existing: %s", dev_path)
|
866 |
|
base.ThrowError("Error in file creation: %", str(err))
|
867 |
|
|
868 |
|
return FileStorage(unique_id, children, size, params)
|
869 |
|
|
870 |
|
|
871 |
|
class PersistentBlockDevice(base.BlockDev):
|
872 |
|
"""A block device with persistent node
|
873 |
|
|
874 |
|
May be either directly attached, or exposed through DM (e.g. dm-multipath).
|
875 |
|
udev helpers are probably required to give persistent, human-friendly
|
876 |
|
names.
|
877 |
|
|
878 |
|
For the time being, pathnames are required to lie under /dev.
|
879 |
|
|
880 |
|
"""
|
881 |
|
def __init__(self, unique_id, children, size, params):
|
882 |
|
"""Attaches to a static block device.
|
883 |
|
|
884 |
|
The unique_id is a path under /dev.
|
885 |
|
|
886 |
|
"""
|
887 |
|
super(PersistentBlockDevice, self).__init__(unique_id, children, size,
|
888 |
|
params)
|
889 |
|
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
|
890 |
|
raise ValueError("Invalid configuration data %s" % str(unique_id))
|
891 |
|
self.dev_path = unique_id[1]
|
892 |
|
if not os.path.realpath(self.dev_path).startswith("/dev/"):
|
893 |
|
raise ValueError("Full path '%s' lies outside /dev" %
|
894 |
|
os.path.realpath(self.dev_path))
|
895 |
|
# TODO: this is just a safety guard checking that we only deal with devices
|
896 |
|
# we know how to handle. In the future this will be integrated with
|
897 |
|
# external storage backends and possible values will probably be collected
|
898 |
|
# from the cluster configuration.
|
899 |
|
if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
|
900 |
|
raise ValueError("Got persistent block device of invalid type: %s" %
|
901 |
|
unique_id[0])
|
902 |
|
|
903 |
|
self.major = self.minor = None
|
904 |
|
self.Attach()
|
905 |
|
|
906 |
|
@classmethod
|
907 |
|
def Create(cls, unique_id, children, size, params, excl_stor):
|
908 |
|
"""Create a new device
|
909 |
|
|
910 |
|
This is a noop, we only return a PersistentBlockDevice instance
|
911 |
|
|
912 |
|
"""
|
913 |
|
if excl_stor:
|
914 |
|
raise errors.ProgrammerError("Persistent block device requested with"
|
915 |
|
" exclusive_storage")
|
916 |
|
return PersistentBlockDevice(unique_id, children, 0, params)
|
917 |
|
|
918 |
|
def Remove(self):
|
919 |
|
"""Remove a device
|
920 |
|
|
921 |
|
This is a noop
|
922 |
|
|
923 |
|
"""
|
924 |
|
pass
|
925 |
|
|
926 |
|
def Rename(self, new_id):
|
927 |
|
"""Rename this device.
|
928 |
|
|
929 |
|
"""
|
930 |
|
base.ThrowError("Rename is not supported for PersistentBlockDev storage")
|
931 |
|
|
932 |
|
def Attach(self):
|
933 |
|
"""Attach to an existing block device.
|
934 |
|
|
935 |
|
|
936 |
|
"""
|
937 |
|
self.attached = False
|
938 |
|
try:
|
939 |
|
st = os.stat(self.dev_path)
|
940 |
|
except OSError, err:
|
941 |
|
logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
|
942 |
|
return False
|
943 |
|
|
944 |
|
if not stat.S_ISBLK(st.st_mode):
|
945 |
|
logging.error("%s is not a block device", self.dev_path)
|
946 |
|
return False
|
947 |
|
|
948 |
|
self.major = os.major(st.st_rdev)
|
949 |
|
self.minor = os.minor(st.st_rdev)
|
950 |
|
self.attached = True
|
951 |
|
|
952 |
|
return True
|
953 |
|
|
954 |
|
def Assemble(self):
|
955 |
|
"""Assemble the device.
|
956 |
|
|
957 |
|
"""
|
958 |
|
pass
|
959 |
|
|
960 |
|
def Shutdown(self):
|
961 |
|
"""Shutdown the device.
|
962 |
|
|
963 |
|
"""
|
964 |
|
pass
|
965 |
|
|
966 |
|
def Open(self, force=False):
|
967 |
|
"""Make the device ready for I/O.
|
968 |
|
|
969 |
|
"""
|
970 |
|
pass
|
971 |
|
|
972 |
|
def Close(self):
|
973 |
|
"""Notifies that the device will no longer be used for I/O.
|
974 |
|
|
975 |
|
"""
|
976 |
|
pass
|
977 |
|
|
978 |
|
def Grow(self, amount, dryrun, backingstore):
|
979 |
|
"""Grow the logical volume.
|
980 |
|
|
981 |
|
"""
|
982 |
|
base.ThrowError("Grow is not supported for PersistentBlockDev storage")
|
983 |
|
|
984 |
|
|
985 |
|
class RADOSBlockDevice(base.BlockDev):
|
986 |
|
"""A RADOS Block Device (rbd).
|
987 |
|
|
988 |
|
This class implements the RADOS Block Device for the backend. You need
|
989 |
|
the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
|
990 |
|
this to be functional.
|
991 |
|
|
992 |
|
"""
|
993 |
|
def __init__(self, unique_id, children, size, params):
|
994 |
|
"""Attaches to an rbd device.
|
995 |
|
|
996 |
|
"""
|
997 |
|
super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
|
998 |
|
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
|
999 |
|
raise ValueError("Invalid configuration data %s" % str(unique_id))
|
1000 |
|
|
1001 |
|
self.driver, self.rbd_name = unique_id
|