]>
Commit | Line | Data |
---|---|---|
d2e6a577 FG |
1 | """ |
2 | API for CRUD lvm tag operations. Follows the Ceph LVM tag naming convention | |
3 | that prefixes tags with ``ceph.`` and uses ``=`` for assignment, and provides | |
4 | set of utilities for interacting with LVM. | |
5 | """ | |
94b18763 FG |
6 | import logging |
7 | import os | |
1adf2230 AA |
8 | import uuid |
9 | from math import floor | |
10 | from ceph_volume import process, util | |
11 | from ceph_volume.exceptions import ( | |
12 | MultipleLVsError, MultipleVGsError, | |
13 | MultiplePVsError, SizeAllocationError | |
14 | ) | |
d2e6a577 | 15 | |
94b18763 FG |
16 | logger = logging.getLogger(__name__) |
17 | ||
d2e6a577 | 18 | |
b5b8bbf5 FG |
19 | def _output_parser(output, fields): |
20 | """ | |
21 | Newer versions of LVM allow ``--reportformat=json``, but older versions, | |
22 | like the one included in Xenial do not. LVM has the ability to filter and | |
23 | format its output so we assume the output will be in a format this parser | |
24 | can handle (using ',' as a delimiter) | |
25 | ||
26 | :param fields: A string, possibly using ',' to group many items, as it | |
27 | would be used on the CLI | |
28 | :param output: The CLI output from the LVM call | |
29 | """ | |
30 | field_items = fields.split(',') | |
31 | report = [] | |
32 | for line in output: | |
33 | # clear the leading/trailing whitespace | |
34 | line = line.strip() | |
35 | ||
36 | # remove the extra '"' in each field | |
37 | line = line.replace('"', '') | |
38 | ||
39 | # prevent moving forward with empty contents | |
40 | if not line: | |
41 | continue | |
42 | ||
43 | # spliting on ';' because that is what the lvm call uses as | |
44 | # '--separator' | |
45 | output_items = [i.strip() for i in line.split(';')] | |
46 | # map the output to the fiels | |
47 | report.append( | |
48 | dict(zip(field_items, output_items)) | |
49 | ) | |
50 | ||
51 | return report | |
52 | ||
53 | ||
1adf2230 AA |
54 | def _splitname_parser(line): |
55 | """ | |
56 | Parses the output from ``dmsetup splitname``, that should contain prefixes | |
57 | (--nameprefixes) and set the separator to ";" | |
58 | ||
59 | Output for /dev/mapper/vg-lv will usually look like:: | |
60 | ||
61 | DM_VG_NAME='/dev/mapper/vg';DM_LV_NAME='lv';DM_LV_LAYER='' | |
62 | ||
63 | ||
64 | The ``VG_NAME`` will usually not be what other callers need (e.g. just 'vg' | |
65 | in the example), so this utility will split ``/dev/mapper/`` out, so that | |
66 | the actual volume group name is kept | |
67 | ||
68 | :returns: dictionary with stripped prefixes | |
69 | """ | |
70 | parts = line[0].split(';') | |
71 | parsed = {} | |
72 | for part in parts: | |
73 | part = part.replace("'", '') | |
74 | key, value = part.split('=') | |
75 | if 'DM_VG_NAME' in key: | |
76 | value = value.split('/dev/mapper/')[-1] | |
77 | key = key.split('DM_')[-1] | |
78 | parsed[key] = value | |
79 | ||
80 | return parsed | |
81 | ||
82 | ||
83 | def sizing(device_size, parts=None, size=None): | |
84 | """ | |
85 | Calculate proper sizing to fully utilize the volume group in the most | |
86 | efficient way possible. To prevent situations where LVM might accept | |
87 | a percentage that is beyond the vg's capabilities, it will refuse with | |
88 | an error when requesting a larger-than-possible parameter, in addition | |
89 | to rounding down calculations. | |
90 | ||
91 | A dictionary with different sizing parameters is returned, to make it | |
92 | easier for others to choose what they need in order to create logical | |
93 | volumes:: | |
94 | ||
95 | >>> sizing(100, parts=2) | |
96 | >>> {'parts': 2, 'percentages': 50, 'sizes': 50} | |
97 | ||
98 | """ | |
99 | if parts is not None and size is not None: | |
100 | raise ValueError( | |
101 | "Cannot process sizing with both parts (%s) and size (%s)" % (parts, size) | |
102 | ) | |
103 | ||
104 | if size and size > device_size: | |
105 | raise SizeAllocationError(size, device_size) | |
106 | ||
107 | def get_percentage(parts): | |
108 | return int(floor(100 / float(parts))) | |
109 | ||
110 | if parts is not None: | |
111 | # Prevent parts being 0, falling back to 1 (100% usage) | |
112 | parts = parts or 1 | |
113 | percentages = get_percentage(parts) | |
114 | ||
115 | if size: | |
116 | parts = int(device_size / size) or 1 | |
117 | percentages = get_percentage(parts) | |
118 | ||
119 | sizes = device_size / parts if parts else int(floor(device_size)) | |
120 | ||
121 | return { | |
122 | 'parts': parts, | |
123 | 'percentages': percentages, | |
124 | 'sizes': int(sizes), | |
125 | } | |
126 | ||
127 | ||
d2e6a577 FG |
128 | def parse_tags(lv_tags): |
129 | """ | |
130 | Return a dictionary mapping of all the tags associated with | |
131 | a Volume from the comma-separated tags coming from the LVM API | |
132 | ||
133 | Input look like:: | |
134 | ||
135 | "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0" | |
136 | ||
137 | For the above example, the expected return value would be:: | |
138 | ||
139 | { | |
140 | "ceph.osd_fsid": "aaa-fff-bbbb", | |
141 | "ceph.osd_id": "0" | |
142 | } | |
143 | """ | |
144 | if not lv_tags: | |
145 | return {} | |
146 | tag_mapping = {} | |
147 | tags = lv_tags.split(',') | |
148 | for tag_assignment in tags: | |
b32b8144 FG |
149 | if not tag_assignment.startswith('ceph.'): |
150 | continue | |
d2e6a577 FG |
151 | key, value = tag_assignment.split('=', 1) |
152 | tag_mapping[key] = value | |
153 | ||
154 | return tag_mapping | |
155 | ||
156 | ||
94b18763 FG |
157 | def _vdo_parents(devices): |
158 | """ | |
159 | It is possible we didn't get a logical volume, or a mapper path, but | |
160 | a device like /dev/sda2, to resolve this, we must look at all the slaves of | |
161 | every single device in /sys/block and if any of those devices is related to | |
162 | VDO devices, then we can add the parent | |
163 | """ | |
164 | parent_devices = [] | |
165 | for parent in os.listdir('/sys/block'): | |
166 | for slave in os.listdir('/sys/block/%s/slaves' % parent): | |
167 | if slave in devices: | |
168 | parent_devices.append('/dev/%s' % parent) | |
169 | parent_devices.append(parent) | |
170 | return parent_devices | |
171 | ||
172 | ||
173 | def _vdo_slaves(vdo_names): | |
174 | """ | |
175 | find all the slaves associated with each vdo name (from realpath) by going | |
176 | into /sys/block/<realpath>/slaves | |
177 | """ | |
178 | devices = [] | |
179 | for vdo_name in vdo_names: | |
180 | mapper_path = '/dev/mapper/%s' % vdo_name | |
181 | if not os.path.exists(mapper_path): | |
182 | continue | |
183 | # resolve the realpath and realname of the vdo mapper | |
184 | vdo_realpath = os.path.realpath(mapper_path) | |
185 | vdo_realname = vdo_realpath.split('/')[-1] | |
186 | slaves_path = '/sys/block/%s/slaves' % vdo_realname | |
187 | if not os.path.exists(slaves_path): | |
188 | continue | |
189 | devices.append(vdo_realpath) | |
190 | devices.append(mapper_path) | |
191 | devices.append(vdo_realname) | |
192 | for slave in os.listdir(slaves_path): | |
193 | devices.append('/dev/%s' % slave) | |
194 | devices.append(slave) | |
195 | return devices | |
196 | ||
197 | ||
198 | def _is_vdo(path): | |
199 | """ | |
200 | A VDO device can be composed from many different devices, go through each | |
201 | one of those devices and its slaves (if any) and correlate them back to | |
202 | /dev/mapper and their realpaths, and then check if they appear as part of | |
203 | /sys/kvdo/<name>/statistics | |
204 | ||
205 | From the realpath of a logical volume, determine if it is a VDO device or | |
206 | not, by correlating it to the presence of the name in | |
207 | /sys/kvdo/<name>/statistics and all the previously captured devices | |
208 | """ | |
209 | if not os.path.isdir('/sys/kvdo'): | |
210 | return False | |
211 | realpath = os.path.realpath(path) | |
212 | realpath_name = realpath.split('/')[-1] | |
213 | devices = [] | |
214 | vdo_names = set() | |
215 | # get all the vdo names | |
216 | for dirname in os.listdir('/sys/kvdo/'): | |
217 | if os.path.isdir('/sys/kvdo/%s/statistics' % dirname): | |
218 | vdo_names.add(dirname) | |
219 | ||
220 | # find all the slaves associated with each vdo name (from realpath) by | |
221 | # going into /sys/block/<realpath>/slaves | |
222 | devices.extend(_vdo_slaves(vdo_names)) | |
223 | ||
224 | # Find all possible parents, looking into slaves that are related to VDO | |
225 | devices.extend(_vdo_parents(devices)) | |
226 | ||
227 | return any([ | |
228 | path in devices, | |
229 | realpath in devices, | |
230 | realpath_name in devices]) | |
231 | ||
232 | ||
233 | def is_vdo(path): | |
234 | """ | |
235 | Detect if a path is backed by VDO, proxying the actual call to _is_vdo so | |
236 | that we can prevent an exception breaking OSD creation. If an exception is | |
237 | raised, it will get captured and logged to file, while returning | |
238 | a ``False``. | |
239 | """ | |
240 | try: | |
241 | if _is_vdo(path): | |
242 | return '1' | |
243 | return '0' | |
244 | except Exception: | |
245 | logger.exception('Unable to properly detect device as VDO: %s', path) | |
246 | return '0' | |
247 | ||
248 | ||
1adf2230 AA |
249 | def dmsetup_splitname(dev): |
250 | """ | |
251 | Run ``dmsetup splitname`` and parse the results. | |
252 | ||
253 | .. warning:: This call does not ensure that the device is correct or that | |
254 | it exists. ``dmsetup`` will happily take a non existing path and still | |
255 | return a 0 exit status. | |
256 | """ | |
257 | command = [ | |
258 | 'dmsetup', 'splitname', '--noheadings', | |
259 | "--separator=';'", '--nameprefixes', dev | |
260 | ] | |
261 | out, err, rc = process.call(command) | |
262 | return _splitname_parser(out) | |
263 | ||
264 | ||
265 | def is_lv(dev, lvs=None): | |
266 | """ | |
267 | Boolean to detect if a device is an LV or not. | |
268 | """ | |
269 | splitname = dmsetup_splitname(dev) | |
270 | # Allowing to optionally pass `lvs` can help reduce repetitive checks for | |
271 | # multiple devices at once. | |
272 | lvs = lvs if lvs is not None else Volumes() | |
273 | if splitname.get('LV_NAME'): | |
274 | lvs.filter(lv_name=splitname['LV_NAME'], vg_name=splitname['VG_NAME']) | |
275 | return len(lvs) > 0 | |
276 | return False | |
277 | ||
278 | ||
d2e6a577 FG |
279 | def get_api_vgs(): |
280 | """ | |
b5b8bbf5 FG |
281 | Return the list of group volumes available in the system using flags to |
282 | include common metadata associated with them | |
d2e6a577 | 283 | |
94b18763 | 284 | Command and sample delimited output should look like:: |
d2e6a577 | 285 | |
1adf2230 | 286 | $ vgs --noheadings --units=g --readonly --separator=';' \ |
b5b8bbf5 FG |
287 | -o vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free |
288 | ubuntubox-vg;1;2;0;wz--n-;299.52g;12.00m | |
289 | osd_vg;3;1;0;wz--n-;29.21g;9.21g | |
d2e6a577 | 290 | |
1adf2230 AA |
291 | To normalize sizing, the units are forced in 'g' which is equivalent to |
292 | gigabytes, which uses multiples of 1024 (as opposed to 1000) | |
d2e6a577 | 293 | """ |
1adf2230 | 294 | fields = 'vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free,vg_free_count' |
d2e6a577 | 295 | stdout, stderr, returncode = process.call( |
91327a77 AA |
296 | ['vgs', '--noheadings', '--readonly', '--units=g', '--separator=";"', '-o', fields], |
297 | verbose_on_failure=False | |
d2e6a577 | 298 | ) |
b5b8bbf5 | 299 | return _output_parser(stdout, fields) |
d2e6a577 FG |
300 | |
301 | ||
302 | def get_api_lvs(): | |
303 | """ | |
304 | Return the list of logical volumes available in the system using flags to include common | |
305 | metadata associated with them | |
306 | ||
94b18763 | 307 | Command and delimited output should look like:: |
d2e6a577 | 308 | |
94b18763 | 309 | $ lvs --noheadings --readonly --separator=';' -o lv_tags,lv_path,lv_name,vg_name |
b5b8bbf5 FG |
310 | ;/dev/ubuntubox-vg/root;root;ubuntubox-vg |
311 | ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg | |
d2e6a577 FG |
312 | |
313 | """ | |
1adf2230 | 314 | fields = 'lv_tags,lv_path,lv_name,vg_name,lv_uuid,lv_size' |
d2e6a577 | 315 | stdout, stderr, returncode = process.call( |
91327a77 AA |
316 | ['lvs', '--noheadings', '--readonly', '--separator=";"', '-o', fields], |
317 | verbose_on_failure=False | |
b5b8bbf5 FG |
318 | ) |
319 | return _output_parser(stdout, fields) | |
d2e6a577 FG |
320 | |
321 | ||
181888fb FG |
322 | def get_api_pvs(): |
323 | """ | |
324 | Return the list of physical volumes configured for lvm and available in the | |
325 | system using flags to include common metadata associated with them like the uuid | |
326 | ||
b32b8144 FG |
327 | This will only return physical volumes set up to work with LVM. |
328 | ||
94b18763 | 329 | Command and delimited output should look like:: |
181888fb | 330 | |
94b18763 | 331 | $ pvs --noheadings --readonly --separator=';' -o pv_name,pv_tags,pv_uuid |
181888fb FG |
332 | /dev/sda1;; |
333 | /dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D | |
334 | ||
335 | """ | |
28e407b8 | 336 | fields = 'pv_name,pv_tags,pv_uuid,vg_name,lv_uuid' |
181888fb | 337 | |
181888fb | 338 | stdout, stderr, returncode = process.call( |
91327a77 AA |
339 | ['pvs', '--no-heading', '--readonly', '--separator=";"', '-o', fields], |
340 | verbose_on_failure=False | |
181888fb FG |
341 | ) |
342 | ||
343 | return _output_parser(stdout, fields) | |
344 | ||
345 | ||
3efd9988 FG |
346 | def get_lv_from_argument(argument): |
347 | """ | |
348 | Helper proxy function that consumes a possible logical volume passed in from the CLI | |
349 | in the form of `vg/lv`, but with some validation so that an argument that is a full | |
350 | path to a device can be ignored | |
351 | """ | |
352 | if argument.startswith('/'): | |
353 | lv = get_lv(lv_path=argument) | |
354 | return lv | |
355 | try: | |
356 | vg_name, lv_name = argument.split('/') | |
357 | except (ValueError, AttributeError): | |
358 | return None | |
359 | return get_lv(lv_name=lv_name, vg_name=vg_name) | |
360 | ||
361 | ||
181888fb | 362 | def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None): |
d2e6a577 FG |
363 | """ |
364 | Return a matching lv for the current system, requiring ``lv_name``, | |
365 | ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv | |
366 | is found. | |
367 | ||
368 | It is useful to use ``tags`` when trying to find a specific logical volume, | |
369 | but it can also lead to multiple lvs being found, since a lot of metadata | |
370 | is shared between lvs of a distinct OSD. | |
371 | """ | |
181888fb | 372 | if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]): |
d2e6a577 FG |
373 | return None |
374 | lvs = Volumes() | |
181888fb FG |
375 | return lvs.get( |
376 | lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_uuid=lv_uuid, | |
377 | lv_tags=lv_tags | |
378 | ) | |
379 | ||
380 | ||
381 | def get_pv(pv_name=None, pv_uuid=None, pv_tags=None): | |
382 | """ | |
383 | Return a matching pv (physical volume) for the current system, requiring | |
384 | ``pv_name``, ``pv_uuid``, or ``pv_tags``. Raises an error if more than one | |
385 | pv is found. | |
386 | """ | |
387 | if not any([pv_name, pv_uuid, pv_tags]): | |
388 | return None | |
389 | pvs = PVolumes() | |
390 | return pvs.get(pv_name=pv_name, pv_uuid=pv_uuid, pv_tags=pv_tags) | |
391 | ||
392 | ||
393 | def create_pv(device): | |
394 | """ | |
395 | Create a physical volume from a device, useful when devices need to be later mapped | |
396 | to journals. | |
397 | """ | |
398 | process.run([ | |
181888fb FG |
399 | 'pvcreate', |
400 | '-v', # verbose | |
401 | '-f', # force it | |
402 | '--yes', # answer yes to any prompts | |
403 | device | |
404 | ]) | |
d2e6a577 FG |
405 | |
406 | ||
1adf2230 | 407 | def create_vg(devices, name=None, name_prefix=None): |
3efd9988 FG |
408 | """ |
409 | Create a Volume Group. Command looks like:: | |
410 | ||
411 | vgcreate --force --yes group_name device | |
412 | ||
413 | Once created the volume group is returned as a ``VolumeGroup`` object | |
1adf2230 AA |
414 | |
415 | :param devices: A list of devices to create a VG. Optionally, a single | |
416 | device (as a string) can be used. | |
417 | :param name: Optionally set the name of the VG, defaults to 'ceph-{uuid}' | |
418 | :param name_prefix: Optionally prefix the name of the VG, which will get combined | |
419 | with a UUID string | |
3efd9988 | 420 | """ |
1adf2230 AA |
421 | if isinstance(devices, set): |
422 | devices = list(devices) | |
423 | if not isinstance(devices, list): | |
424 | devices = [devices] | |
425 | if name_prefix: | |
426 | name = "%s-%s" % (name_prefix, str(uuid.uuid4())) | |
427 | elif name is None: | |
428 | name = "ceph-%s" % str(uuid.uuid4()) | |
3efd9988 | 429 | process.run([ |
3efd9988 FG |
430 | 'vgcreate', |
431 | '--force', | |
432 | '--yes', | |
1adf2230 | 433 | name] + devices |
3efd9988 FG |
434 | ) |
435 | ||
436 | vg = get_vg(vg_name=name) | |
437 | return vg | |
438 | ||
439 | ||
1adf2230 AA |
440 | def extend_vg(vg, devices): |
441 | """ | |
442 | Extend a Volume Group. Command looks like:: | |
443 | ||
444 | vgextend --force --yes group_name [device, ...] | |
445 | ||
446 | Once created the volume group is extended and returned as a ``VolumeGroup`` object | |
447 | ||
448 | :param vg: A VolumeGroup object | |
449 | :param devices: A list of devices to extend the VG. Optionally, a single | |
450 | device (as a string) can be used. | |
451 | """ | |
452 | if not isinstance(devices, list): | |
453 | devices = [devices] | |
454 | process.run([ | |
455 | 'vgextend', | |
456 | '--force', | |
457 | '--yes', | |
458 | vg.name] + devices | |
459 | ) | |
460 | ||
461 | vg = get_vg(vg_name=vg.name) | |
462 | return vg | |
463 | ||
464 | ||
b32b8144 FG |
465 | def remove_vg(vg_name): |
466 | """ | |
467 | Removes a volume group. | |
468 | """ | |
94b18763 | 469 | fail_msg = "Unable to remove vg %s" % vg_name |
b32b8144 FG |
470 | process.run( |
471 | [ | |
472 | 'vgremove', | |
473 | '-v', # verbose | |
474 | '-f', # force it | |
475 | vg_name | |
476 | ], | |
477 | fail_msg=fail_msg, | |
478 | ) | |
479 | ||
480 | ||
481 | def remove_pv(pv_name): | |
482 | """ | |
91327a77 AA |
483 | Removes a physical volume using a double `-f` to prevent prompts and fully |
484 | remove anything related to LVM. This is tremendously destructive, but so is all other actions | |
485 | when zapping a device. | |
486 | ||
487 | In the case where multiple PVs are found, it will ignore that fact and | |
488 | continue with the removal, specifically in the case of messages like:: | |
489 | ||
490 | WARNING: PV $UUID /dev/DEV-1 was already found on /dev/DEV-2 | |
491 | ||
492 | These situations can be avoided with custom filtering rules, which this API | |
493 | cannot handle while accommodating custom user filters. | |
b32b8144 | 494 | """ |
94b18763 | 495 | fail_msg = "Unable to remove vg %s" % pv_name |
b32b8144 FG |
496 | process.run( |
497 | [ | |
498 | 'pvremove', | |
499 | '-v', # verbose | |
500 | '-f', # force it | |
91327a77 | 501 | '-f', # force it |
b32b8144 FG |
502 | pv_name |
503 | ], | |
504 | fail_msg=fail_msg, | |
505 | ) | |
506 | ||
507 | ||
91327a77 | 508 | def remove_lv(lv): |
3efd9988 FG |
509 | """ |
510 | Removes a logical volume given it's absolute path. | |
511 | ||
512 | Will return True if the lv is successfully removed or | |
513 | raises a RuntimeError if the removal fails. | |
91327a77 AA |
514 | |
515 | :param lv: A ``Volume`` object or the path for an LV | |
3efd9988 | 516 | """ |
91327a77 AA |
517 | if isinstance(lv, Volume): |
518 | path = lv.lv_path | |
519 | else: | |
520 | path = lv | |
521 | ||
3efd9988 FG |
522 | stdout, stderr, returncode = process.call( |
523 | [ | |
3efd9988 FG |
524 | 'lvremove', |
525 | '-v', # verbose | |
526 | '-f', # force it | |
527 | path | |
528 | ], | |
529 | show_command=True, | |
530 | terminal_verbose=True, | |
531 | ) | |
532 | if returncode != 0: | |
94b18763 | 533 | raise RuntimeError("Unable to remove %s" % path) |
3efd9988 FG |
534 | return True |
535 | ||
536 | ||
1adf2230 | 537 | def create_lv(name, group, extents=None, size=None, tags=None, uuid_name=False): |
d2e6a577 FG |
538 | """ |
539 | Create a Logical Volume in a Volume Group. Command looks like:: | |
540 | ||
541 | lvcreate -L 50G -n gfslv vg0 | |
542 | ||
3efd9988 FG |
543 | ``name``, ``group``, are required. If ``size`` is provided it must follow |
544 | lvm's size notation (like 1G, or 20M). Tags are an optional dictionary and is expected to | |
545 | conform to the convention of prefixing them with "ceph." like:: | |
d2e6a577 | 546 | |
3efd9988 | 547 | {"ceph.block_device": "/dev/ceph/osd-1"} |
1adf2230 AA |
548 | |
549 | :param uuid_name: Optionally combine the ``name`` with UUID to ensure uniqueness | |
d2e6a577 | 550 | """ |
1adf2230 AA |
551 | if uuid_name: |
552 | name = '%s-%s' % (name, uuid.uuid4()) | |
553 | if tags is None: | |
554 | tags = { | |
555 | "ceph.osd_id": "null", | |
556 | "ceph.type": "null", | |
557 | "ceph.cluster_fsid": "null", | |
558 | "ceph.osd_fsid": "null", | |
559 | } | |
560 | ||
d2e6a577 FG |
561 | # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations |
562 | type_path_tag = { | |
563 | 'journal': 'ceph.journal_device', | |
564 | 'data': 'ceph.data_device', | |
3efd9988 FG |
565 | 'block': 'ceph.block_device', |
566 | 'wal': 'ceph.wal_device', | |
567 | 'db': 'ceph.db_device', | |
568 | 'lockbox': 'ceph.lockbox_device', # XXX might not ever need this lockbox sorcery | |
d2e6a577 FG |
569 | } |
570 | if size: | |
571 | process.run([ | |
d2e6a577 FG |
572 | 'lvcreate', |
573 | '--yes', | |
574 | '-L', | |
3efd9988 | 575 | '%s' % size, |
d2e6a577 FG |
576 | '-n', name, group |
577 | ]) | |
1adf2230 AA |
578 | elif extents: |
579 | process.run([ | |
580 | 'lvcreate', | |
581 | '--yes', | |
582 | '-l', | |
583 | '%s' % extents, | |
584 | '-n', name, group | |
585 | ]) | |
d2e6a577 FG |
586 | # create the lv with all the space available, this is needed because the |
587 | # system call is different for LVM | |
588 | else: | |
589 | process.run([ | |
d2e6a577 FG |
590 | 'lvcreate', |
591 | '--yes', | |
592 | '-l', | |
593 | '100%FREE', | |
594 | '-n', name, group | |
595 | ]) | |
596 | ||
597 | lv = get_lv(lv_name=name, vg_name=group) | |
3efd9988 | 598 | lv.set_tags(tags) |
d2e6a577 FG |
599 | |
600 | # when creating a distinct type, the caller doesn't know what the path will | |
601 | # be so this function will set it after creation using the mapping | |
3efd9988 FG |
602 | path_tag = type_path_tag.get(tags.get('ceph.type')) |
603 | if path_tag: | |
604 | lv.set_tags( | |
605 | {path_tag: lv.lv_path} | |
606 | ) | |
d2e6a577 FG |
607 | return lv |
608 | ||
609 | ||
1adf2230 AA |
610 | def create_lvs(volume_group, parts=None, size=None, name_prefix='ceph-lv'): |
611 | """ | |
612 | Create multiple Logical Volumes from a Volume Group by calculating the | |
613 | proper extents from ``parts`` or ``size``. A custom prefix can be used | |
614 | (defaults to ``ceph-lv``), these names are always suffixed with a uuid. | |
615 | ||
616 | LV creation in ceph-volume will require tags, this is expected to be | |
617 | pre-computed by callers who know Ceph metadata like OSD IDs and FSIDs. It | |
618 | will probably not be the case when mass-creating LVs, so common/default | |
619 | tags will be set to ``"null"``. | |
620 | ||
621 | .. note:: LVs that are not in use can be detected by querying LVM for tags that are | |
622 | set to ``"null"``. | |
623 | ||
624 | :param volume_group: The volume group (vg) to use for LV creation | |
625 | :type group: ``VolumeGroup()`` object | |
626 | :param parts: Number of LVs to create *instead of* ``size``. | |
627 | :type parts: int | |
628 | :param size: Size (in gigabytes) of LVs to create, e.g. "as many 10gb LVs as possible" | |
629 | :type size: int | |
630 | :param extents: The number of LVM extents to use to create the LV. Useful if looking to have | |
631 | accurate LV sizes (LVM rounds sizes otherwise) | |
632 | """ | |
633 | if parts is None and size is None: | |
634 | # fallback to just one part (using 100% of the vg) | |
635 | parts = 1 | |
636 | lvs = [] | |
637 | tags = { | |
638 | "ceph.osd_id": "null", | |
639 | "ceph.type": "null", | |
640 | "ceph.cluster_fsid": "null", | |
641 | "ceph.osd_fsid": "null", | |
642 | } | |
643 | sizing = volume_group.sizing(parts=parts, size=size) | |
644 | for part in range(0, sizing['parts']): | |
645 | size = sizing['sizes'] | |
646 | extents = sizing['extents'] | |
647 | lv_name = '%s-%s' % (name_prefix, uuid.uuid4()) | |
648 | lvs.append( | |
649 | create_lv(lv_name, volume_group.name, extents=extents, tags=tags) | |
650 | ) | |
651 | return lvs | |
652 | ||
653 | ||
d2e6a577 FG |
654 | def get_vg(vg_name=None, vg_tags=None): |
655 | """ | |
656 | Return a matching vg for the current system, requires ``vg_name`` or | |
657 | ``tags``. Raises an error if more than one vg is found. | |
658 | ||
659 | It is useful to use ``tags`` when trying to find a specific volume group, | |
660 | but it can also lead to multiple vgs being found. | |
661 | """ | |
662 | if not any([vg_name, vg_tags]): | |
663 | return None | |
664 | vgs = VolumeGroups() | |
665 | return vgs.get(vg_name=vg_name, vg_tags=vg_tags) | |
666 | ||
667 | ||
668 | class VolumeGroups(list): | |
669 | """ | |
670 | A list of all known volume groups for the current system, with the ability | |
671 | to filter them via keyword arguments. | |
672 | """ | |
673 | ||
674 | def __init__(self): | |
675 | self._populate() | |
676 | ||
677 | def _populate(self): | |
678 | # get all the vgs in the current system | |
679 | for vg_item in get_api_vgs(): | |
680 | self.append(VolumeGroup(**vg_item)) | |
681 | ||
682 | def _purge(self): | |
683 | """ | |
684 | Deplete all the items in the list, used internally only so that we can | |
685 | dynamically allocate the items when filtering without the concern of | |
686 | messing up the contents | |
687 | """ | |
688 | self[:] = [] | |
689 | ||
690 | def _filter(self, vg_name=None, vg_tags=None): | |
691 | """ | |
692 | The actual method that filters using a new list. Useful so that other | |
693 | methods that do not want to alter the contents of the list (e.g. | |
694 | ``self.find``) can operate safely. | |
695 | ||
696 | .. note:: ``vg_tags`` is not yet implemented | |
697 | """ | |
698 | filtered = [i for i in self] | |
699 | if vg_name: | |
700 | filtered = [i for i in filtered if i.vg_name == vg_name] | |
701 | ||
702 | # at this point, `filtered` has either all the volumes in self or is an | |
703 | # actual filtered list if any filters were applied | |
704 | if vg_tags: | |
705 | tag_filtered = [] | |
181888fb FG |
706 | for volume in filtered: |
707 | matches = all(volume.tags.get(k) == str(v) for k, v in vg_tags.items()) | |
708 | if matches: | |
709 | tag_filtered.append(volume) | |
d2e6a577 FG |
710 | return tag_filtered |
711 | ||
712 | return filtered | |
713 | ||
714 | def filter(self, vg_name=None, vg_tags=None): | |
715 | """ | |
716 | Filter out groups on top level attributes like ``vg_name`` or by | |
717 | ``vg_tags`` where a dict is required. For example, to find a Ceph group | |
718 | with dmcache as the type, the filter would look like:: | |
719 | ||
720 | vg_tags={'ceph.type': 'dmcache'} | |
721 | ||
722 | .. warning:: These tags are not documented because they are currently | |
723 | unused, but are here to maintain API consistency | |
724 | """ | |
725 | if not any([vg_name, vg_tags]): | |
726 | raise TypeError('.filter() requires vg_name or vg_tags (none given)') | |
727 | # first find the filtered volumes with the values in self | |
728 | filtered_groups = self._filter( | |
729 | vg_name=vg_name, | |
730 | vg_tags=vg_tags | |
731 | ) | |
732 | # then purge everything | |
733 | self._purge() | |
734 | # and add the filtered items | |
735 | self.extend(filtered_groups) | |
736 | ||
737 | def get(self, vg_name=None, vg_tags=None): | |
738 | """ | |
739 | This is a bit expensive, since it will try to filter out all the | |
740 | matching items in the list, filter them out applying anything that was | |
741 | added and return the matching item. | |
742 | ||
743 | This method does *not* alter the list, and it will raise an error if | |
744 | multiple VGs are matched | |
745 | ||
746 | It is useful to use ``tags`` when trying to find a specific volume group, | |
747 | but it can also lead to multiple vgs being found (although unlikely) | |
748 | """ | |
749 | if not any([vg_name, vg_tags]): | |
750 | return None | |
751 | vgs = self._filter( | |
752 | vg_name=vg_name, | |
753 | vg_tags=vg_tags | |
754 | ) | |
755 | if not vgs: | |
756 | return None | |
757 | if len(vgs) > 1: | |
758 | # this is probably never going to happen, but it is here to keep | |
759 | # the API code consistent | |
760 | raise MultipleVGsError(vg_name) | |
761 | return vgs[0] | |
762 | ||
763 | ||
764 | class Volumes(list): | |
765 | """ | |
766 | A list of all known (logical) volumes for the current system, with the ability | |
767 | to filter them via keyword arguments. | |
768 | """ | |
769 | ||
770 | def __init__(self): | |
771 | self._populate() | |
772 | ||
773 | def _populate(self): | |
774 | # get all the lvs in the current system | |
775 | for lv_item in get_api_lvs(): | |
776 | self.append(Volume(**lv_item)) | |
777 | ||
778 | def _purge(self): | |
779 | """ | |
94b18763 | 780 | Delete all the items in the list, used internally only so that we can |
d2e6a577 FG |
781 | dynamically allocate the items when filtering without the concern of |
782 | messing up the contents | |
783 | """ | |
784 | self[:] = [] | |
785 | ||
181888fb | 786 | def _filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None): |
d2e6a577 FG |
787 | """ |
788 | The actual method that filters using a new list. Useful so that other | |
789 | methods that do not want to alter the contents of the list (e.g. | |
790 | ``self.find``) can operate safely. | |
791 | """ | |
792 | filtered = [i for i in self] | |
793 | if lv_name: | |
794 | filtered = [i for i in filtered if i.lv_name == lv_name] | |
795 | ||
796 | if vg_name: | |
797 | filtered = [i for i in filtered if i.vg_name == vg_name] | |
798 | ||
181888fb FG |
799 | if lv_uuid: |
800 | filtered = [i for i in filtered if i.lv_uuid == lv_uuid] | |
801 | ||
d2e6a577 FG |
802 | if lv_path: |
803 | filtered = [i for i in filtered if i.lv_path == lv_path] | |
804 | ||
805 | # at this point, `filtered` has either all the volumes in self or is an | |
806 | # actual filtered list if any filters were applied | |
807 | if lv_tags: | |
808 | tag_filtered = [] | |
181888fb FG |
809 | for volume in filtered: |
810 | # all the tags we got need to match on the volume | |
811 | matches = all(volume.tags.get(k) == str(v) for k, v in lv_tags.items()) | |
812 | if matches: | |
813 | tag_filtered.append(volume) | |
d2e6a577 FG |
814 | return tag_filtered |
815 | ||
816 | return filtered | |
817 | ||
181888fb | 818 | def filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None): |
d2e6a577 FG |
819 | """ |
820 | Filter out volumes on top level attributes like ``lv_name`` or by | |
821 | ``lv_tags`` where a dict is required. For example, to find a volume | |
822 | that has an OSD ID of 0, the filter would look like:: | |
823 | ||
824 | lv_tags={'ceph.osd_id': '0'} | |
825 | ||
826 | """ | |
181888fb FG |
827 | if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]): |
828 | raise TypeError('.filter() requires lv_name, vg_name, lv_path, lv_uuid, or tags (none given)') | |
d2e6a577 FG |
829 | # first find the filtered volumes with the values in self |
830 | filtered_volumes = self._filter( | |
831 | lv_name=lv_name, | |
832 | vg_name=vg_name, | |
833 | lv_path=lv_path, | |
181888fb | 834 | lv_uuid=lv_uuid, |
d2e6a577 FG |
835 | lv_tags=lv_tags |
836 | ) | |
837 | # then purge everything | |
838 | self._purge() | |
839 | # and add the filtered items | |
840 | self.extend(filtered_volumes) | |
841 | ||
181888fb | 842 | def get(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None): |
d2e6a577 FG |
843 | """ |
844 | This is a bit expensive, since it will try to filter out all the | |
845 | matching items in the list, filter them out applying anything that was | |
846 | added and return the matching item. | |
847 | ||
848 | This method does *not* alter the list, and it will raise an error if | |
849 | multiple LVs are matched | |
850 | ||
851 | It is useful to use ``tags`` when trying to find a specific logical volume, | |
852 | but it can also lead to multiple lvs being found, since a lot of metadata | |
853 | is shared between lvs of a distinct OSD. | |
854 | """ | |
181888fb | 855 | if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]): |
d2e6a577 FG |
856 | return None |
857 | lvs = self._filter( | |
858 | lv_name=lv_name, | |
859 | vg_name=vg_name, | |
860 | lv_path=lv_path, | |
181888fb | 861 | lv_uuid=lv_uuid, |
d2e6a577 FG |
862 | lv_tags=lv_tags |
863 | ) | |
864 | if not lvs: | |
865 | return None | |
866 | if len(lvs) > 1: | |
867 | raise MultipleLVsError(lv_name, lv_path) | |
868 | return lvs[0] | |
869 | ||
870 | ||
181888fb FG |
871 | class PVolumes(list): |
872 | """ | |
873 | A list of all known (physical) volumes for the current system, with the ability | |
874 | to filter them via keyword arguments. | |
875 | """ | |
876 | ||
877 | def __init__(self): | |
878 | self._populate() | |
879 | ||
880 | def _populate(self): | |
881 | # get all the pvs in the current system | |
882 | for pv_item in get_api_pvs(): | |
883 | self.append(PVolume(**pv_item)) | |
884 | ||
885 | def _purge(self): | |
886 | """ | |
887 | Deplete all the items in the list, used internally only so that we can | |
888 | dynamically allocate the items when filtering without the concern of | |
889 | messing up the contents | |
890 | """ | |
891 | self[:] = [] | |
892 | ||
893 | def _filter(self, pv_name=None, pv_uuid=None, pv_tags=None): | |
894 | """ | |
895 | The actual method that filters using a new list. Useful so that other | |
896 | methods that do not want to alter the contents of the list (e.g. | |
897 | ``self.find``) can operate safely. | |
898 | """ | |
899 | filtered = [i for i in self] | |
900 | if pv_name: | |
901 | filtered = [i for i in filtered if i.pv_name == pv_name] | |
902 | ||
903 | if pv_uuid: | |
904 | filtered = [i for i in filtered if i.pv_uuid == pv_uuid] | |
905 | ||
906 | # at this point, `filtered` has either all the physical volumes in self | |
907 | # or is an actual filtered list if any filters were applied | |
908 | if pv_tags: | |
909 | tag_filtered = [] | |
910 | for pvolume in filtered: | |
911 | matches = all(pvolume.tags.get(k) == str(v) for k, v in pv_tags.items()) | |
912 | if matches: | |
913 | tag_filtered.append(pvolume) | |
914 | # return the tag_filtered pvolumes here, the `filtered` list is no | |
915 | # longer useable | |
916 | return tag_filtered | |
917 | ||
918 | return filtered | |
919 | ||
920 | def filter(self, pv_name=None, pv_uuid=None, pv_tags=None): | |
921 | """ | |
922 | Filter out volumes on top level attributes like ``pv_name`` or by | |
923 | ``pv_tags`` where a dict is required. For example, to find a physical volume | |
924 | that has an OSD ID of 0, the filter would look like:: | |
925 | ||
926 | pv_tags={'ceph.osd_id': '0'} | |
927 | ||
928 | """ | |
929 | if not any([pv_name, pv_uuid, pv_tags]): | |
930 | raise TypeError('.filter() requires pv_name, pv_uuid, or pv_tags (none given)') | |
931 | # first find the filtered volumes with the values in self | |
932 | filtered_volumes = self._filter( | |
933 | pv_name=pv_name, | |
934 | pv_uuid=pv_uuid, | |
935 | pv_tags=pv_tags | |
936 | ) | |
937 | # then purge everything | |
938 | self._purge() | |
939 | # and add the filtered items | |
940 | self.extend(filtered_volumes) | |
941 | ||
942 | def get(self, pv_name=None, pv_uuid=None, pv_tags=None): | |
943 | """ | |
944 | This is a bit expensive, since it will try to filter out all the | |
945 | matching items in the list, filter them out applying anything that was | |
946 | added and return the matching item. | |
947 | ||
948 | This method does *not* alter the list, and it will raise an error if | |
949 | multiple pvs are matched | |
950 | ||
951 | It is useful to use ``tags`` when trying to find a specific logical volume, | |
952 | but it can also lead to multiple pvs being found, since a lot of metadata | |
953 | is shared between pvs of a distinct OSD. | |
954 | """ | |
955 | if not any([pv_name, pv_uuid, pv_tags]): | |
956 | return None | |
957 | pvs = self._filter( | |
958 | pv_name=pv_name, | |
959 | pv_uuid=pv_uuid, | |
960 | pv_tags=pv_tags | |
961 | ) | |
962 | if not pvs: | |
963 | return None | |
1adf2230 | 964 | if len(pvs) > 1 and pv_tags: |
181888fb FG |
965 | raise MultiplePVsError(pv_name) |
966 | return pvs[0] | |
967 | ||
968 | ||
d2e6a577 FG |
969 | class VolumeGroup(object): |
970 | """ | |
971 | Represents an LVM group, with some top-level attributes like ``vg_name`` | |
972 | """ | |
973 | ||
974 | def __init__(self, **kw): | |
975 | for k, v in kw.items(): | |
976 | setattr(self, k, v) | |
977 | self.name = kw['vg_name'] | |
978 | self.tags = parse_tags(kw.get('vg_tags', '')) | |
979 | ||
980 | def __str__(self): | |
981 | return '<%s>' % self.name | |
982 | ||
983 | def __repr__(self): | |
984 | return self.__str__() | |
985 | ||
1adf2230 AA |
986 | def _parse_size(self, size): |
987 | error_msg = "Unable to convert vg size to integer: '%s'" % str(size) | |
988 | try: | |
989 | integer, _ = size.split('g') | |
990 | except ValueError: | |
991 | logger.exception(error_msg) | |
992 | raise RuntimeError(error_msg) | |
993 | ||
994 | return util.str_to_int(integer) | |
995 | ||
996 | @property | |
997 | def free(self): | |
998 | """ | |
999 | Parse the available size in gigabytes from the ``vg_free`` attribute, that | |
1000 | will be a string with a character ('g') to indicate gigabytes in size. | |
1001 | Returns a rounded down integer to ease internal operations:: | |
1002 | ||
1003 | >>> data_vg.vg_free | |
1004 | '0.01g' | |
1005 | >>> data_vg.size | |
1006 | 0 | |
1007 | """ | |
1008 | return self._parse_size(self.vg_free) | |
1009 | ||
1010 | @property | |
1011 | def size(self): | |
1012 | """ | |
1013 | Parse the size in gigabytes from the ``vg_size`` attribute, that | |
1014 | will be a string with a character ('g') to indicate gigabytes in size. | |
1015 | Returns a rounded down integer to ease internal operations:: | |
1016 | ||
1017 | >>> data_vg.vg_size | |
1018 | '1024.9g' | |
1019 | >>> data_vg.size | |
1020 | 1024 | |
1021 | """ | |
1022 | return self._parse_size(self.vg_size) | |
1023 | ||
1024 | def sizing(self, parts=None, size=None): | |
1025 | """ | |
1026 | Calculate proper sizing to fully utilize the volume group in the most | |
1027 | efficient way possible. To prevent situations where LVM might accept | |
1028 | a percentage that is beyond the vg's capabilities, it will refuse with | |
1029 | an error when requesting a larger-than-possible parameter, in addition | |
1030 | to rounding down calculations. | |
1031 | ||
1032 | A dictionary with different sizing parameters is returned, to make it | |
1033 | easier for others to choose what they need in order to create logical | |
1034 | volumes:: | |
1035 | ||
1036 | >>> data_vg.free | |
1037 | 1024 | |
1038 | >>> data_vg.sizing(parts=4) | |
1039 | {'parts': 4, 'sizes': 256, 'percentages': 25} | |
1040 | >>> data_vg.sizing(size=512) | |
1041 | {'parts': 2, 'sizes': 512, 'percentages': 50} | |
1042 | ||
1043 | ||
1044 | :param parts: Number of parts to create LVs from | |
1045 | :param size: Size in gigabytes to divide the VG into | |
1046 | ||
1047 | :raises SizeAllocationError: When requested size cannot be allocated with | |
1048 | :raises ValueError: If both ``parts`` and ``size`` are given | |
1049 | """ | |
1050 | if parts is not None and size is not None: | |
1051 | raise ValueError( | |
1052 | "Cannot process sizing with both parts (%s) and size (%s)" % (parts, size) | |
1053 | ) | |
1054 | ||
1055 | # if size is given we need to map that to extents so that we avoid | |
1056 | # issues when trying to get this right with a size in gigabytes find | |
1057 | # the percentage first, cheating, because these values are thrown out | |
1058 | vg_free_count = util.str_to_int(self.vg_free_count) | |
1059 | ||
1060 | if size: | |
1061 | extents = int(size * vg_free_count / self.free) | |
1062 | disk_sizing = sizing(self.free, size=size, parts=parts) | |
1063 | else: | |
1064 | if parts is not None: | |
1065 | # Prevent parts being 0, falling back to 1 (100% usage) | |
1066 | parts = parts or 1 | |
1067 | size = int(self.free / parts) | |
1068 | extents = size * vg_free_count / self.free | |
1069 | disk_sizing = sizing(self.free, parts=parts) | |
1070 | ||
1071 | extent_sizing = sizing(vg_free_count, size=extents) | |
1072 | ||
1073 | disk_sizing['extents'] = int(extents) | |
1074 | disk_sizing['percentages'] = extent_sizing['percentages'] | |
1075 | return disk_sizing | |
1076 | ||
d2e6a577 FG |
1077 | |
1078 | class Volume(object): | |
1079 | """ | |
1080 | Represents a Logical Volume from LVM, with some top-level attributes like | |
1081 | ``lv_name`` and parsed tags as a dictionary of key/value pairs. | |
1082 | """ | |
1083 | ||
1084 | def __init__(self, **kw): | |
1085 | for k, v in kw.items(): | |
1086 | setattr(self, k, v) | |
1087 | self.lv_api = kw | |
1088 | self.name = kw['lv_name'] | |
1089 | self.tags = parse_tags(kw['lv_tags']) | |
3a9019d9 | 1090 | self.encrypted = self.tags.get('ceph.encrypted', '0') == '1' |
91327a77 | 1091 | self.used_by_ceph = 'ceph.osd_id' in self.tags |
d2e6a577 FG |
1092 | |
1093 | def __str__(self): | |
1094 | return '<%s>' % self.lv_api['lv_path'] | |
1095 | ||
1096 | def __repr__(self): | |
1097 | return self.__str__() | |
1098 | ||
3efd9988 FG |
1099 | def as_dict(self): |
1100 | obj = {} | |
1101 | obj.update(self.lv_api) | |
1102 | obj['tags'] = self.tags | |
1103 | obj['name'] = self.name | |
1104 | obj['type'] = self.tags['ceph.type'] | |
1105 | obj['path'] = self.lv_path | |
1106 | return obj | |
1107 | ||
91327a77 AA |
1108 | def report(self): |
1109 | if not self.used_by_ceph: | |
1110 | return { | |
1111 | 'name': self.lv_name, | |
1112 | 'comment': 'not used by ceph' | |
1113 | } | |
1114 | else: | |
1115 | type_ = self.tags['ceph.type'] | |
1116 | report = { | |
1117 | 'name': self.lv_name, | |
1118 | 'osd_id': self.tags['ceph.osd_id'], | |
1119 | 'cluster_name': self.tags['ceph.cluster_name'], | |
1120 | 'type': type_, | |
1121 | 'osd_fsid': self.tags['ceph.osd_fsid'], | |
1122 | 'cluster_fsid': self.tags['ceph.cluster_fsid'], | |
1123 | } | |
1124 | type_uuid = '{}_uuid'.format(type_) | |
1125 | report[type_uuid] = self.tags['ceph.{}'.format(type_uuid)] | |
1126 | return report | |
1127 | ||
3efd9988 FG |
1128 | def clear_tags(self): |
1129 | """ | |
1130 | Removes all tags from the Logical Volume. | |
1131 | """ | |
1132 | for k, v in self.tags.items(): | |
1133 | tag = "%s=%s" % (k, v) | |
b32b8144 | 1134 | process.run(['lvchange', '--deltag', tag, self.lv_path]) |
3efd9988 | 1135 | |
d2e6a577 FG |
1136 | def set_tags(self, tags): |
1137 | """ | |
1138 | :param tags: A dictionary of tag names and values, like:: | |
1139 | ||
1140 | { | |
1141 | "ceph.osd_fsid": "aaa-fff-bbbb", | |
1142 | "ceph.osd_id": "0" | |
1143 | } | |
1144 | ||
1145 | At the end of all modifications, the tags are refreshed to reflect | |
1146 | LVM's most current view. | |
1147 | """ | |
1148 | for k, v in tags.items(): | |
1149 | self.set_tag(k, v) | |
1150 | # after setting all the tags, refresh them for the current object, use the | |
1151 | # lv_* identifiers to filter because those shouldn't change | |
1152 | lv_object = get_lv(lv_name=self.lv_name, lv_path=self.lv_path) | |
1153 | self.tags = lv_object.tags | |
1154 | ||
1155 | def set_tag(self, key, value): | |
1156 | """ | |
1157 | Set the key/value pair as an LVM tag. Does not "refresh" the values of | |
1158 | the current object for its tags. Meant to be a "fire and forget" type | |
1159 | of modification. | |
1160 | """ | |
1161 | # remove it first if it exists | |
1162 | if self.tags.get(key): | |
1163 | current_value = self.tags[key] | |
1164 | tag = "%s=%s" % (key, current_value) | |
b32b8144 | 1165 | process.call(['lvchange', '--deltag', tag, self.lv_api['lv_path']]) |
d2e6a577 FG |
1166 | |
1167 | process.call( | |
1168 | [ | |
b32b8144 | 1169 | 'lvchange', |
d2e6a577 FG |
1170 | '--addtag', '%s=%s' % (key, value), self.lv_path |
1171 | ] | |
1172 | ) | |
181888fb FG |
1173 | |
1174 | ||
1175 | class PVolume(object): | |
1176 | """ | |
1177 | Represents a Physical Volume from LVM, with some top-level attributes like | |
1178 | ``pv_name`` and parsed tags as a dictionary of key/value pairs. | |
1179 | """ | |
1180 | ||
1181 | def __init__(self, **kw): | |
1182 | for k, v in kw.items(): | |
1183 | setattr(self, k, v) | |
1184 | self.pv_api = kw | |
1185 | self.name = kw['pv_name'] | |
1186 | self.tags = parse_tags(kw['pv_tags']) | |
1187 | ||
1188 | def __str__(self): | |
1189 | return '<%s>' % self.pv_api['pv_name'] | |
1190 | ||
1191 | def __repr__(self): | |
1192 | return self.__str__() | |
1193 | ||
1194 | def set_tags(self, tags): | |
1195 | """ | |
1196 | :param tags: A dictionary of tag names and values, like:: | |
1197 | ||
1198 | { | |
1199 | "ceph.osd_fsid": "aaa-fff-bbbb", | |
1200 | "ceph.osd_id": "0" | |
1201 | } | |
1202 | ||
1203 | At the end of all modifications, the tags are refreshed to reflect | |
1204 | LVM's most current view. | |
1205 | """ | |
1206 | for k, v in tags.items(): | |
1207 | self.set_tag(k, v) | |
1208 | # after setting all the tags, refresh them for the current object, use the | |
1209 | # pv_* identifiers to filter because those shouldn't change | |
1210 | pv_object = get_pv(pv_name=self.pv_name, pv_uuid=self.pv_uuid) | |
1211 | self.tags = pv_object.tags | |
1212 | ||
1213 | def set_tag(self, key, value): | |
1214 | """ | |
1215 | Set the key/value pair as an LVM tag. Does not "refresh" the values of | |
1216 | the current object for its tags. Meant to be a "fire and forget" type | |
1217 | of modification. | |
1218 | ||
1219 | **warning**: Altering tags on a PV has to be done ensuring that the | |
1220 | device is actually the one intended. ``pv_name`` is *not* a persistent | |
1221 | value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make | |
1222 | sure the device getting changed is the one needed. | |
1223 | """ | |
1224 | # remove it first if it exists | |
1225 | if self.tags.get(key): | |
1226 | current_value = self.tags[key] | |
1227 | tag = "%s=%s" % (key, current_value) | |
b32b8144 | 1228 | process.call(['pvchange', '--deltag', tag, self.pv_name]) |
181888fb FG |
1229 | |
1230 | process.call( | |
1231 | [ | |
b32b8144 | 1232 | 'pvchange', |
181888fb FG |
1233 | '--addtag', '%s=%s' % (key, value), self.pv_name |
1234 | ] | |
1235 | ) |