]>
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( |
1adf2230 | 296 | ['vgs', '--noheadings', '--readonly', '--units=g', '--separator=";"', '-o', fields] |
d2e6a577 | 297 | ) |
b5b8bbf5 | 298 | return _output_parser(stdout, fields) |
d2e6a577 FG |
299 | |
300 | ||
301 | def get_api_lvs(): | |
302 | """ | |
303 | Return the list of logical volumes available in the system using flags to include common | |
304 | metadata associated with them | |
305 | ||
94b18763 | 306 | Command and delimited output should look like:: |
d2e6a577 | 307 | |
94b18763 | 308 | $ lvs --noheadings --readonly --separator=';' -o lv_tags,lv_path,lv_name,vg_name |
b5b8bbf5 FG |
309 | ;/dev/ubuntubox-vg/root;root;ubuntubox-vg |
310 | ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg | |
d2e6a577 FG |
311 | |
312 | """ | |
1adf2230 | 313 | fields = 'lv_tags,lv_path,lv_name,vg_name,lv_uuid,lv_size' |
d2e6a577 | 314 | stdout, stderr, returncode = process.call( |
94b18763 | 315 | ['lvs', '--noheadings', '--readonly', '--separator=";"', '-o', fields] |
b5b8bbf5 FG |
316 | ) |
317 | return _output_parser(stdout, fields) | |
d2e6a577 FG |
318 | |
319 | ||
181888fb FG |
320 | def get_api_pvs(): |
321 | """ | |
322 | Return the list of physical volumes configured for lvm and available in the | |
323 | system using flags to include common metadata associated with them like the uuid | |
324 | ||
b32b8144 FG |
325 | This will only return physical volumes set up to work with LVM. |
326 | ||
94b18763 | 327 | Command and delimited output should look like:: |
181888fb | 328 | |
94b18763 | 329 | $ pvs --noheadings --readonly --separator=';' -o pv_name,pv_tags,pv_uuid |
181888fb FG |
330 | /dev/sda1;; |
331 | /dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D | |
332 | ||
333 | """ | |
28e407b8 | 334 | fields = 'pv_name,pv_tags,pv_uuid,vg_name,lv_uuid' |
181888fb | 335 | |
181888fb | 336 | stdout, stderr, returncode = process.call( |
94b18763 | 337 | ['pvs', '--no-heading', '--readonly', '--separator=";"', '-o', fields] |
181888fb FG |
338 | ) |
339 | ||
340 | return _output_parser(stdout, fields) | |
341 | ||
342 | ||
3efd9988 FG |
343 | def get_lv_from_argument(argument): |
344 | """ | |
345 | Helper proxy function that consumes a possible logical volume passed in from the CLI | |
346 | in the form of `vg/lv`, but with some validation so that an argument that is a full | |
347 | path to a device can be ignored | |
348 | """ | |
349 | if argument.startswith('/'): | |
350 | lv = get_lv(lv_path=argument) | |
351 | return lv | |
352 | try: | |
353 | vg_name, lv_name = argument.split('/') | |
354 | except (ValueError, AttributeError): | |
355 | return None | |
356 | return get_lv(lv_name=lv_name, vg_name=vg_name) | |
357 | ||
358 | ||
181888fb | 359 | def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None): |
d2e6a577 FG |
360 | """ |
361 | Return a matching lv for the current system, requiring ``lv_name``, | |
362 | ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv | |
363 | is found. | |
364 | ||
365 | It is useful to use ``tags`` when trying to find a specific logical volume, | |
366 | but it can also lead to multiple lvs being found, since a lot of metadata | |
367 | is shared between lvs of a distinct OSD. | |
368 | """ | |
181888fb | 369 | if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]): |
d2e6a577 FG |
370 | return None |
371 | lvs = Volumes() | |
181888fb FG |
372 | return lvs.get( |
373 | lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_uuid=lv_uuid, | |
374 | lv_tags=lv_tags | |
375 | ) | |
376 | ||
377 | ||
378 | def get_pv(pv_name=None, pv_uuid=None, pv_tags=None): | |
379 | """ | |
380 | Return a matching pv (physical volume) for the current system, requiring | |
381 | ``pv_name``, ``pv_uuid``, or ``pv_tags``. Raises an error if more than one | |
382 | pv is found. | |
383 | """ | |
384 | if not any([pv_name, pv_uuid, pv_tags]): | |
385 | return None | |
386 | pvs = PVolumes() | |
387 | return pvs.get(pv_name=pv_name, pv_uuid=pv_uuid, pv_tags=pv_tags) | |
388 | ||
389 | ||
390 | def create_pv(device): | |
391 | """ | |
392 | Create a physical volume from a device, useful when devices need to be later mapped | |
393 | to journals. | |
394 | """ | |
395 | process.run([ | |
181888fb FG |
396 | 'pvcreate', |
397 | '-v', # verbose | |
398 | '-f', # force it | |
399 | '--yes', # answer yes to any prompts | |
400 | device | |
401 | ]) | |
d2e6a577 FG |
402 | |
403 | ||
1adf2230 | 404 | def create_vg(devices, name=None, name_prefix=None): |
3efd9988 FG |
405 | """ |
406 | Create a Volume Group. Command looks like:: | |
407 | ||
408 | vgcreate --force --yes group_name device | |
409 | ||
410 | Once created the volume group is returned as a ``VolumeGroup`` object | |
1adf2230 AA |
411 | |
412 | :param devices: A list of devices to create a VG. Optionally, a single | |
413 | device (as a string) can be used. | |
414 | :param name: Optionally set the name of the VG, defaults to 'ceph-{uuid}' | |
415 | :param name_prefix: Optionally prefix the name of the VG, which will get combined | |
416 | with a UUID string | |
3efd9988 | 417 | """ |
1adf2230 AA |
418 | if isinstance(devices, set): |
419 | devices = list(devices) | |
420 | if not isinstance(devices, list): | |
421 | devices = [devices] | |
422 | if name_prefix: | |
423 | name = "%s-%s" % (name_prefix, str(uuid.uuid4())) | |
424 | elif name is None: | |
425 | name = "ceph-%s" % str(uuid.uuid4()) | |
3efd9988 | 426 | process.run([ |
3efd9988 FG |
427 | 'vgcreate', |
428 | '--force', | |
429 | '--yes', | |
1adf2230 | 430 | name] + devices |
3efd9988 FG |
431 | ) |
432 | ||
433 | vg = get_vg(vg_name=name) | |
434 | return vg | |
435 | ||
436 | ||
1adf2230 AA |
437 | def extend_vg(vg, devices): |
438 | """ | |
439 | Extend a Volume Group. Command looks like:: | |
440 | ||
441 | vgextend --force --yes group_name [device, ...] | |
442 | ||
443 | Once created the volume group is extended and returned as a ``VolumeGroup`` object | |
444 | ||
445 | :param vg: A VolumeGroup object | |
446 | :param devices: A list of devices to extend the VG. Optionally, a single | |
447 | device (as a string) can be used. | |
448 | """ | |
449 | if not isinstance(devices, list): | |
450 | devices = [devices] | |
451 | process.run([ | |
452 | 'vgextend', | |
453 | '--force', | |
454 | '--yes', | |
455 | vg.name] + devices | |
456 | ) | |
457 | ||
458 | vg = get_vg(vg_name=vg.name) | |
459 | return vg | |
460 | ||
461 | ||
b32b8144 FG |
462 | def remove_vg(vg_name): |
463 | """ | |
464 | Removes a volume group. | |
465 | """ | |
94b18763 | 466 | fail_msg = "Unable to remove vg %s" % vg_name |
b32b8144 FG |
467 | process.run( |
468 | [ | |
469 | 'vgremove', | |
470 | '-v', # verbose | |
471 | '-f', # force it | |
472 | vg_name | |
473 | ], | |
474 | fail_msg=fail_msg, | |
475 | ) | |
476 | ||
477 | ||
478 | def remove_pv(pv_name): | |
479 | """ | |
480 | Removes a physical volume. | |
481 | """ | |
94b18763 | 482 | fail_msg = "Unable to remove vg %s" % pv_name |
b32b8144 FG |
483 | process.run( |
484 | [ | |
485 | 'pvremove', | |
486 | '-v', # verbose | |
487 | '-f', # force it | |
488 | pv_name | |
489 | ], | |
490 | fail_msg=fail_msg, | |
491 | ) | |
492 | ||
493 | ||
3efd9988 FG |
494 | def remove_lv(path): |
495 | """ | |
496 | Removes a logical volume given it's absolute path. | |
497 | ||
498 | Will return True if the lv is successfully removed or | |
499 | raises a RuntimeError if the removal fails. | |
500 | """ | |
501 | stdout, stderr, returncode = process.call( | |
502 | [ | |
3efd9988 FG |
503 | 'lvremove', |
504 | '-v', # verbose | |
505 | '-f', # force it | |
506 | path | |
507 | ], | |
508 | show_command=True, | |
509 | terminal_verbose=True, | |
510 | ) | |
511 | if returncode != 0: | |
94b18763 | 512 | raise RuntimeError("Unable to remove %s" % path) |
3efd9988 FG |
513 | return True |
514 | ||
515 | ||
1adf2230 | 516 | def create_lv(name, group, extents=None, size=None, tags=None, uuid_name=False): |
d2e6a577 FG |
517 | """ |
518 | Create a Logical Volume in a Volume Group. Command looks like:: | |
519 | ||
520 | lvcreate -L 50G -n gfslv vg0 | |
521 | ||
3efd9988 FG |
522 | ``name``, ``group``, are required. If ``size`` is provided it must follow |
523 | lvm's size notation (like 1G, or 20M). Tags are an optional dictionary and is expected to | |
524 | conform to the convention of prefixing them with "ceph." like:: | |
d2e6a577 | 525 | |
3efd9988 | 526 | {"ceph.block_device": "/dev/ceph/osd-1"} |
1adf2230 AA |
527 | |
528 | :param uuid_name: Optionally combine the ``name`` with UUID to ensure uniqueness | |
d2e6a577 | 529 | """ |
1adf2230 AA |
530 | if uuid_name: |
531 | name = '%s-%s' % (name, uuid.uuid4()) | |
532 | if tags is None: | |
533 | tags = { | |
534 | "ceph.osd_id": "null", | |
535 | "ceph.type": "null", | |
536 | "ceph.cluster_fsid": "null", | |
537 | "ceph.osd_fsid": "null", | |
538 | } | |
539 | ||
d2e6a577 FG |
540 | # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations |
541 | type_path_tag = { | |
542 | 'journal': 'ceph.journal_device', | |
543 | 'data': 'ceph.data_device', | |
3efd9988 FG |
544 | 'block': 'ceph.block_device', |
545 | 'wal': 'ceph.wal_device', | |
546 | 'db': 'ceph.db_device', | |
547 | 'lockbox': 'ceph.lockbox_device', # XXX might not ever need this lockbox sorcery | |
d2e6a577 FG |
548 | } |
549 | if size: | |
550 | process.run([ | |
d2e6a577 FG |
551 | 'lvcreate', |
552 | '--yes', | |
553 | '-L', | |
3efd9988 | 554 | '%s' % size, |
d2e6a577 FG |
555 | '-n', name, group |
556 | ]) | |
1adf2230 AA |
557 | elif extents: |
558 | process.run([ | |
559 | 'lvcreate', | |
560 | '--yes', | |
561 | '-l', | |
562 | '%s' % extents, | |
563 | '-n', name, group | |
564 | ]) | |
d2e6a577 FG |
565 | # create the lv with all the space available, this is needed because the |
566 | # system call is different for LVM | |
567 | else: | |
568 | process.run([ | |
d2e6a577 FG |
569 | 'lvcreate', |
570 | '--yes', | |
571 | '-l', | |
572 | '100%FREE', | |
573 | '-n', name, group | |
574 | ]) | |
575 | ||
576 | lv = get_lv(lv_name=name, vg_name=group) | |
3efd9988 | 577 | lv.set_tags(tags) |
d2e6a577 FG |
578 | |
579 | # when creating a distinct type, the caller doesn't know what the path will | |
580 | # be so this function will set it after creation using the mapping | |
3efd9988 FG |
581 | path_tag = type_path_tag.get(tags.get('ceph.type')) |
582 | if path_tag: | |
583 | lv.set_tags( | |
584 | {path_tag: lv.lv_path} | |
585 | ) | |
d2e6a577 FG |
586 | return lv |
587 | ||
588 | ||
1adf2230 AA |
589 | def create_lvs(volume_group, parts=None, size=None, name_prefix='ceph-lv'): |
590 | """ | |
591 | Create multiple Logical Volumes from a Volume Group by calculating the | |
592 | proper extents from ``parts`` or ``size``. A custom prefix can be used | |
593 | (defaults to ``ceph-lv``), these names are always suffixed with a uuid. | |
594 | ||
595 | LV creation in ceph-volume will require tags, this is expected to be | |
596 | pre-computed by callers who know Ceph metadata like OSD IDs and FSIDs. It | |
597 | will probably not be the case when mass-creating LVs, so common/default | |
598 | tags will be set to ``"null"``. | |
599 | ||
600 | .. note:: LVs that are not in use can be detected by querying LVM for tags that are | |
601 | set to ``"null"``. | |
602 | ||
603 | :param volume_group: The volume group (vg) to use for LV creation | |
604 | :type group: ``VolumeGroup()`` object | |
605 | :param parts: Number of LVs to create *instead of* ``size``. | |
606 | :type parts: int | |
607 | :param size: Size (in gigabytes) of LVs to create, e.g. "as many 10gb LVs as possible" | |
608 | :type size: int | |
609 | :param extents: The number of LVM extents to use to create the LV. Useful if looking to have | |
610 | accurate LV sizes (LVM rounds sizes otherwise) | |
611 | """ | |
612 | if parts is None and size is None: | |
613 | # fallback to just one part (using 100% of the vg) | |
614 | parts = 1 | |
615 | lvs = [] | |
616 | tags = { | |
617 | "ceph.osd_id": "null", | |
618 | "ceph.type": "null", | |
619 | "ceph.cluster_fsid": "null", | |
620 | "ceph.osd_fsid": "null", | |
621 | } | |
622 | sizing = volume_group.sizing(parts=parts, size=size) | |
623 | for part in range(0, sizing['parts']): | |
624 | size = sizing['sizes'] | |
625 | extents = sizing['extents'] | |
626 | lv_name = '%s-%s' % (name_prefix, uuid.uuid4()) | |
627 | lvs.append( | |
628 | create_lv(lv_name, volume_group.name, extents=extents, tags=tags) | |
629 | ) | |
630 | return lvs | |
631 | ||
632 | ||
d2e6a577 FG |
633 | def get_vg(vg_name=None, vg_tags=None): |
634 | """ | |
635 | Return a matching vg for the current system, requires ``vg_name`` or | |
636 | ``tags``. Raises an error if more than one vg is found. | |
637 | ||
638 | It is useful to use ``tags`` when trying to find a specific volume group, | |
639 | but it can also lead to multiple vgs being found. | |
640 | """ | |
641 | if not any([vg_name, vg_tags]): | |
642 | return None | |
643 | vgs = VolumeGroups() | |
644 | return vgs.get(vg_name=vg_name, vg_tags=vg_tags) | |
645 | ||
646 | ||
647 | class VolumeGroups(list): | |
648 | """ | |
649 | A list of all known volume groups for the current system, with the ability | |
650 | to filter them via keyword arguments. | |
651 | """ | |
652 | ||
653 | def __init__(self): | |
654 | self._populate() | |
655 | ||
656 | def _populate(self): | |
657 | # get all the vgs in the current system | |
658 | for vg_item in get_api_vgs(): | |
659 | self.append(VolumeGroup(**vg_item)) | |
660 | ||
661 | def _purge(self): | |
662 | """ | |
663 | Deplete all the items in the list, used internally only so that we can | |
664 | dynamically allocate the items when filtering without the concern of | |
665 | messing up the contents | |
666 | """ | |
667 | self[:] = [] | |
668 | ||
669 | def _filter(self, vg_name=None, vg_tags=None): | |
670 | """ | |
671 | The actual method that filters using a new list. Useful so that other | |
672 | methods that do not want to alter the contents of the list (e.g. | |
673 | ``self.find``) can operate safely. | |
674 | ||
675 | .. note:: ``vg_tags`` is not yet implemented | |
676 | """ | |
677 | filtered = [i for i in self] | |
678 | if vg_name: | |
679 | filtered = [i for i in filtered if i.vg_name == vg_name] | |
680 | ||
681 | # at this point, `filtered` has either all the volumes in self or is an | |
682 | # actual filtered list if any filters were applied | |
683 | if vg_tags: | |
684 | tag_filtered = [] | |
181888fb FG |
685 | for volume in filtered: |
686 | matches = all(volume.tags.get(k) == str(v) for k, v in vg_tags.items()) | |
687 | if matches: | |
688 | tag_filtered.append(volume) | |
d2e6a577 FG |
689 | return tag_filtered |
690 | ||
691 | return filtered | |
692 | ||
693 | def filter(self, vg_name=None, vg_tags=None): | |
694 | """ | |
695 | Filter out groups on top level attributes like ``vg_name`` or by | |
696 | ``vg_tags`` where a dict is required. For example, to find a Ceph group | |
697 | with dmcache as the type, the filter would look like:: | |
698 | ||
699 | vg_tags={'ceph.type': 'dmcache'} | |
700 | ||
701 | .. warning:: These tags are not documented because they are currently | |
702 | unused, but are here to maintain API consistency | |
703 | """ | |
704 | if not any([vg_name, vg_tags]): | |
705 | raise TypeError('.filter() requires vg_name or vg_tags (none given)') | |
706 | # first find the filtered volumes with the values in self | |
707 | filtered_groups = self._filter( | |
708 | vg_name=vg_name, | |
709 | vg_tags=vg_tags | |
710 | ) | |
711 | # then purge everything | |
712 | self._purge() | |
713 | # and add the filtered items | |
714 | self.extend(filtered_groups) | |
715 | ||
716 | def get(self, vg_name=None, vg_tags=None): | |
717 | """ | |
718 | This is a bit expensive, since it will try to filter out all the | |
719 | matching items in the list, filter them out applying anything that was | |
720 | added and return the matching item. | |
721 | ||
722 | This method does *not* alter the list, and it will raise an error if | |
723 | multiple VGs are matched | |
724 | ||
725 | It is useful to use ``tags`` when trying to find a specific volume group, | |
726 | but it can also lead to multiple vgs being found (although unlikely) | |
727 | """ | |
728 | if not any([vg_name, vg_tags]): | |
729 | return None | |
730 | vgs = self._filter( | |
731 | vg_name=vg_name, | |
732 | vg_tags=vg_tags | |
733 | ) | |
734 | if not vgs: | |
735 | return None | |
736 | if len(vgs) > 1: | |
737 | # this is probably never going to happen, but it is here to keep | |
738 | # the API code consistent | |
739 | raise MultipleVGsError(vg_name) | |
740 | return vgs[0] | |
741 | ||
742 | ||
743 | class Volumes(list): | |
744 | """ | |
745 | A list of all known (logical) volumes for the current system, with the ability | |
746 | to filter them via keyword arguments. | |
747 | """ | |
748 | ||
749 | def __init__(self): | |
750 | self._populate() | |
751 | ||
752 | def _populate(self): | |
753 | # get all the lvs in the current system | |
754 | for lv_item in get_api_lvs(): | |
755 | self.append(Volume(**lv_item)) | |
756 | ||
757 | def _purge(self): | |
758 | """ | |
94b18763 | 759 | Delete all the items in the list, used internally only so that we can |
d2e6a577 FG |
760 | dynamically allocate the items when filtering without the concern of |
761 | messing up the contents | |
762 | """ | |
763 | self[:] = [] | |
764 | ||
181888fb | 765 | def _filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None): |
d2e6a577 FG |
766 | """ |
767 | The actual method that filters using a new list. Useful so that other | |
768 | methods that do not want to alter the contents of the list (e.g. | |
769 | ``self.find``) can operate safely. | |
770 | """ | |
771 | filtered = [i for i in self] | |
772 | if lv_name: | |
773 | filtered = [i for i in filtered if i.lv_name == lv_name] | |
774 | ||
775 | if vg_name: | |
776 | filtered = [i for i in filtered if i.vg_name == vg_name] | |
777 | ||
181888fb FG |
778 | if lv_uuid: |
779 | filtered = [i for i in filtered if i.lv_uuid == lv_uuid] | |
780 | ||
d2e6a577 FG |
781 | if lv_path: |
782 | filtered = [i for i in filtered if i.lv_path == lv_path] | |
783 | ||
784 | # at this point, `filtered` has either all the volumes in self or is an | |
785 | # actual filtered list if any filters were applied | |
786 | if lv_tags: | |
787 | tag_filtered = [] | |
181888fb FG |
788 | for volume in filtered: |
789 | # all the tags we got need to match on the volume | |
790 | matches = all(volume.tags.get(k) == str(v) for k, v in lv_tags.items()) | |
791 | if matches: | |
792 | tag_filtered.append(volume) | |
d2e6a577 FG |
793 | return tag_filtered |
794 | ||
795 | return filtered | |
796 | ||
181888fb | 797 | def filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None): |
d2e6a577 FG |
798 | """ |
799 | Filter out volumes on top level attributes like ``lv_name`` or by | |
800 | ``lv_tags`` where a dict is required. For example, to find a volume | |
801 | that has an OSD ID of 0, the filter would look like:: | |
802 | ||
803 | lv_tags={'ceph.osd_id': '0'} | |
804 | ||
805 | """ | |
181888fb FG |
806 | if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]): |
807 | raise TypeError('.filter() requires lv_name, vg_name, lv_path, lv_uuid, or tags (none given)') | |
d2e6a577 FG |
808 | # first find the filtered volumes with the values in self |
809 | filtered_volumes = self._filter( | |
810 | lv_name=lv_name, | |
811 | vg_name=vg_name, | |
812 | lv_path=lv_path, | |
181888fb | 813 | lv_uuid=lv_uuid, |
d2e6a577 FG |
814 | lv_tags=lv_tags |
815 | ) | |
816 | # then purge everything | |
817 | self._purge() | |
818 | # and add the filtered items | |
819 | self.extend(filtered_volumes) | |
820 | ||
181888fb | 821 | def get(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None): |
d2e6a577 FG |
822 | """ |
823 | This is a bit expensive, since it will try to filter out all the | |
824 | matching items in the list, filter them out applying anything that was | |
825 | added and return the matching item. | |
826 | ||
827 | This method does *not* alter the list, and it will raise an error if | |
828 | multiple LVs are matched | |
829 | ||
830 | It is useful to use ``tags`` when trying to find a specific logical volume, | |
831 | but it can also lead to multiple lvs being found, since a lot of metadata | |
832 | is shared between lvs of a distinct OSD. | |
833 | """ | |
181888fb | 834 | if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]): |
d2e6a577 FG |
835 | return None |
836 | lvs = self._filter( | |
837 | lv_name=lv_name, | |
838 | vg_name=vg_name, | |
839 | lv_path=lv_path, | |
181888fb | 840 | lv_uuid=lv_uuid, |
d2e6a577 FG |
841 | lv_tags=lv_tags |
842 | ) | |
843 | if not lvs: | |
844 | return None | |
845 | if len(lvs) > 1: | |
846 | raise MultipleLVsError(lv_name, lv_path) | |
847 | return lvs[0] | |
848 | ||
849 | ||
181888fb FG |
850 | class PVolumes(list): |
851 | """ | |
852 | A list of all known (physical) volumes for the current system, with the ability | |
853 | to filter them via keyword arguments. | |
854 | """ | |
855 | ||
856 | def __init__(self): | |
857 | self._populate() | |
858 | ||
859 | def _populate(self): | |
860 | # get all the pvs in the current system | |
861 | for pv_item in get_api_pvs(): | |
862 | self.append(PVolume(**pv_item)) | |
863 | ||
864 | def _purge(self): | |
865 | """ | |
866 | Deplete all the items in the list, used internally only so that we can | |
867 | dynamically allocate the items when filtering without the concern of | |
868 | messing up the contents | |
869 | """ | |
870 | self[:] = [] | |
871 | ||
872 | def _filter(self, pv_name=None, pv_uuid=None, pv_tags=None): | |
873 | """ | |
874 | The actual method that filters using a new list. Useful so that other | |
875 | methods that do not want to alter the contents of the list (e.g. | |
876 | ``self.find``) can operate safely. | |
877 | """ | |
878 | filtered = [i for i in self] | |
879 | if pv_name: | |
880 | filtered = [i for i in filtered if i.pv_name == pv_name] | |
881 | ||
882 | if pv_uuid: | |
883 | filtered = [i for i in filtered if i.pv_uuid == pv_uuid] | |
884 | ||
885 | # at this point, `filtered` has either all the physical volumes in self | |
886 | # or is an actual filtered list if any filters were applied | |
887 | if pv_tags: | |
888 | tag_filtered = [] | |
889 | for pvolume in filtered: | |
890 | matches = all(pvolume.tags.get(k) == str(v) for k, v in pv_tags.items()) | |
891 | if matches: | |
892 | tag_filtered.append(pvolume) | |
893 | # return the tag_filtered pvolumes here, the `filtered` list is no | |
894 | # longer useable | |
895 | return tag_filtered | |
896 | ||
897 | return filtered | |
898 | ||
899 | def filter(self, pv_name=None, pv_uuid=None, pv_tags=None): | |
900 | """ | |
901 | Filter out volumes on top level attributes like ``pv_name`` or by | |
902 | ``pv_tags`` where a dict is required. For example, to find a physical volume | |
903 | that has an OSD ID of 0, the filter would look like:: | |
904 | ||
905 | pv_tags={'ceph.osd_id': '0'} | |
906 | ||
907 | """ | |
908 | if not any([pv_name, pv_uuid, pv_tags]): | |
909 | raise TypeError('.filter() requires pv_name, pv_uuid, or pv_tags (none given)') | |
910 | # first find the filtered volumes with the values in self | |
911 | filtered_volumes = self._filter( | |
912 | pv_name=pv_name, | |
913 | pv_uuid=pv_uuid, | |
914 | pv_tags=pv_tags | |
915 | ) | |
916 | # then purge everything | |
917 | self._purge() | |
918 | # and add the filtered items | |
919 | self.extend(filtered_volumes) | |
920 | ||
921 | def get(self, pv_name=None, pv_uuid=None, pv_tags=None): | |
922 | """ | |
923 | This is a bit expensive, since it will try to filter out all the | |
924 | matching items in the list, filter them out applying anything that was | |
925 | added and return the matching item. | |
926 | ||
927 | This method does *not* alter the list, and it will raise an error if | |
928 | multiple pvs are matched | |
929 | ||
930 | It is useful to use ``tags`` when trying to find a specific logical volume, | |
931 | but it can also lead to multiple pvs being found, since a lot of metadata | |
932 | is shared between pvs of a distinct OSD. | |
933 | """ | |
934 | if not any([pv_name, pv_uuid, pv_tags]): | |
935 | return None | |
936 | pvs = self._filter( | |
937 | pv_name=pv_name, | |
938 | pv_uuid=pv_uuid, | |
939 | pv_tags=pv_tags | |
940 | ) | |
941 | if not pvs: | |
942 | return None | |
1adf2230 | 943 | if len(pvs) > 1 and pv_tags: |
181888fb FG |
944 | raise MultiplePVsError(pv_name) |
945 | return pvs[0] | |
946 | ||
947 | ||
d2e6a577 FG |
948 | class VolumeGroup(object): |
949 | """ | |
950 | Represents an LVM group, with some top-level attributes like ``vg_name`` | |
951 | """ | |
952 | ||
953 | def __init__(self, **kw): | |
954 | for k, v in kw.items(): | |
955 | setattr(self, k, v) | |
956 | self.name = kw['vg_name'] | |
957 | self.tags = parse_tags(kw.get('vg_tags', '')) | |
958 | ||
959 | def __str__(self): | |
960 | return '<%s>' % self.name | |
961 | ||
962 | def __repr__(self): | |
963 | return self.__str__() | |
964 | ||
1adf2230 AA |
965 | def _parse_size(self, size): |
966 | error_msg = "Unable to convert vg size to integer: '%s'" % str(size) | |
967 | try: | |
968 | integer, _ = size.split('g') | |
969 | except ValueError: | |
970 | logger.exception(error_msg) | |
971 | raise RuntimeError(error_msg) | |
972 | ||
973 | return util.str_to_int(integer) | |
974 | ||
975 | @property | |
976 | def free(self): | |
977 | """ | |
978 | Parse the available size in gigabytes from the ``vg_free`` attribute, that | |
979 | will be a string with a character ('g') to indicate gigabytes in size. | |
980 | Returns a rounded down integer to ease internal operations:: | |
981 | ||
982 | >>> data_vg.vg_free | |
983 | '0.01g' | |
984 | >>> data_vg.size | |
985 | 0 | |
986 | """ | |
987 | return self._parse_size(self.vg_free) | |
988 | ||
989 | @property | |
990 | def size(self): | |
991 | """ | |
992 | Parse the size in gigabytes from the ``vg_size`` attribute, that | |
993 | will be a string with a character ('g') to indicate gigabytes in size. | |
994 | Returns a rounded down integer to ease internal operations:: | |
995 | ||
996 | >>> data_vg.vg_size | |
997 | '1024.9g' | |
998 | >>> data_vg.size | |
999 | 1024 | |
1000 | """ | |
1001 | return self._parse_size(self.vg_size) | |
1002 | ||
1003 | def sizing(self, parts=None, size=None): | |
1004 | """ | |
1005 | Calculate proper sizing to fully utilize the volume group in the most | |
1006 | efficient way possible. To prevent situations where LVM might accept | |
1007 | a percentage that is beyond the vg's capabilities, it will refuse with | |
1008 | an error when requesting a larger-than-possible parameter, in addition | |
1009 | to rounding down calculations. | |
1010 | ||
1011 | A dictionary with different sizing parameters is returned, to make it | |
1012 | easier for others to choose what they need in order to create logical | |
1013 | volumes:: | |
1014 | ||
1015 | >>> data_vg.free | |
1016 | 1024 | |
1017 | >>> data_vg.sizing(parts=4) | |
1018 | {'parts': 4, 'sizes': 256, 'percentages': 25} | |
1019 | >>> data_vg.sizing(size=512) | |
1020 | {'parts': 2, 'sizes': 512, 'percentages': 50} | |
1021 | ||
1022 | ||
1023 | :param parts: Number of parts to create LVs from | |
1024 | :param size: Size in gigabytes to divide the VG into | |
1025 | ||
1026 | :raises SizeAllocationError: When requested size cannot be allocated with | |
1027 | :raises ValueError: If both ``parts`` and ``size`` are given | |
1028 | """ | |
1029 | if parts is not None and size is not None: | |
1030 | raise ValueError( | |
1031 | "Cannot process sizing with both parts (%s) and size (%s)" % (parts, size) | |
1032 | ) | |
1033 | ||
1034 | # if size is given we need to map that to extents so that we avoid | |
1035 | # issues when trying to get this right with a size in gigabytes find | |
1036 | # the percentage first, cheating, because these values are thrown out | |
1037 | vg_free_count = util.str_to_int(self.vg_free_count) | |
1038 | ||
1039 | if size: | |
1040 | extents = int(size * vg_free_count / self.free) | |
1041 | disk_sizing = sizing(self.free, size=size, parts=parts) | |
1042 | else: | |
1043 | if parts is not None: | |
1044 | # Prevent parts being 0, falling back to 1 (100% usage) | |
1045 | parts = parts or 1 | |
1046 | size = int(self.free / parts) | |
1047 | extents = size * vg_free_count / self.free | |
1048 | disk_sizing = sizing(self.free, parts=parts) | |
1049 | ||
1050 | extent_sizing = sizing(vg_free_count, size=extents) | |
1051 | ||
1052 | disk_sizing['extents'] = int(extents) | |
1053 | disk_sizing['percentages'] = extent_sizing['percentages'] | |
1054 | return disk_sizing | |
1055 | ||
d2e6a577 FG |
1056 | |
1057 | class Volume(object): | |
1058 | """ | |
1059 | Represents a Logical Volume from LVM, with some top-level attributes like | |
1060 | ``lv_name`` and parsed tags as a dictionary of key/value pairs. | |
1061 | """ | |
1062 | ||
1063 | def __init__(self, **kw): | |
1064 | for k, v in kw.items(): | |
1065 | setattr(self, k, v) | |
1066 | self.lv_api = kw | |
1067 | self.name = kw['lv_name'] | |
1068 | self.tags = parse_tags(kw['lv_tags']) | |
3a9019d9 | 1069 | self.encrypted = self.tags.get('ceph.encrypted', '0') == '1' |
d2e6a577 FG |
1070 | |
1071 | def __str__(self): | |
1072 | return '<%s>' % self.lv_api['lv_path'] | |
1073 | ||
1074 | def __repr__(self): | |
1075 | return self.__str__() | |
1076 | ||
3efd9988 FG |
1077 | def as_dict(self): |
1078 | obj = {} | |
1079 | obj.update(self.lv_api) | |
1080 | obj['tags'] = self.tags | |
1081 | obj['name'] = self.name | |
1082 | obj['type'] = self.tags['ceph.type'] | |
1083 | obj['path'] = self.lv_path | |
1084 | return obj | |
1085 | ||
1086 | def clear_tags(self): | |
1087 | """ | |
1088 | Removes all tags from the Logical Volume. | |
1089 | """ | |
1090 | for k, v in self.tags.items(): | |
1091 | tag = "%s=%s" % (k, v) | |
b32b8144 | 1092 | process.run(['lvchange', '--deltag', tag, self.lv_path]) |
3efd9988 | 1093 | |
d2e6a577 FG |
1094 | def set_tags(self, tags): |
1095 | """ | |
1096 | :param tags: A dictionary of tag names and values, like:: | |
1097 | ||
1098 | { | |
1099 | "ceph.osd_fsid": "aaa-fff-bbbb", | |
1100 | "ceph.osd_id": "0" | |
1101 | } | |
1102 | ||
1103 | At the end of all modifications, the tags are refreshed to reflect | |
1104 | LVM's most current view. | |
1105 | """ | |
1106 | for k, v in tags.items(): | |
1107 | self.set_tag(k, v) | |
1108 | # after setting all the tags, refresh them for the current object, use the | |
1109 | # lv_* identifiers to filter because those shouldn't change | |
1110 | lv_object = get_lv(lv_name=self.lv_name, lv_path=self.lv_path) | |
1111 | self.tags = lv_object.tags | |
1112 | ||
1113 | def set_tag(self, key, value): | |
1114 | """ | |
1115 | Set the key/value pair as an LVM tag. Does not "refresh" the values of | |
1116 | the current object for its tags. Meant to be a "fire and forget" type | |
1117 | of modification. | |
1118 | """ | |
1119 | # remove it first if it exists | |
1120 | if self.tags.get(key): | |
1121 | current_value = self.tags[key] | |
1122 | tag = "%s=%s" % (key, current_value) | |
b32b8144 | 1123 | process.call(['lvchange', '--deltag', tag, self.lv_api['lv_path']]) |
d2e6a577 FG |
1124 | |
1125 | process.call( | |
1126 | [ | |
b32b8144 | 1127 | 'lvchange', |
d2e6a577 FG |
1128 | '--addtag', '%s=%s' % (key, value), self.lv_path |
1129 | ] | |
1130 | ) | |
181888fb FG |
1131 | |
1132 | ||
1133 | class PVolume(object): | |
1134 | """ | |
1135 | Represents a Physical Volume from LVM, with some top-level attributes like | |
1136 | ``pv_name`` and parsed tags as a dictionary of key/value pairs. | |
1137 | """ | |
1138 | ||
1139 | def __init__(self, **kw): | |
1140 | for k, v in kw.items(): | |
1141 | setattr(self, k, v) | |
1142 | self.pv_api = kw | |
1143 | self.name = kw['pv_name'] | |
1144 | self.tags = parse_tags(kw['pv_tags']) | |
1145 | ||
1146 | def __str__(self): | |
1147 | return '<%s>' % self.pv_api['pv_name'] | |
1148 | ||
1149 | def __repr__(self): | |
1150 | return self.__str__() | |
1151 | ||
1152 | def set_tags(self, tags): | |
1153 | """ | |
1154 | :param tags: A dictionary of tag names and values, like:: | |
1155 | ||
1156 | { | |
1157 | "ceph.osd_fsid": "aaa-fff-bbbb", | |
1158 | "ceph.osd_id": "0" | |
1159 | } | |
1160 | ||
1161 | At the end of all modifications, the tags are refreshed to reflect | |
1162 | LVM's most current view. | |
1163 | """ | |
1164 | for k, v in tags.items(): | |
1165 | self.set_tag(k, v) | |
1166 | # after setting all the tags, refresh them for the current object, use the | |
1167 | # pv_* identifiers to filter because those shouldn't change | |
1168 | pv_object = get_pv(pv_name=self.pv_name, pv_uuid=self.pv_uuid) | |
1169 | self.tags = pv_object.tags | |
1170 | ||
1171 | def set_tag(self, key, value): | |
1172 | """ | |
1173 | Set the key/value pair as an LVM tag. Does not "refresh" the values of | |
1174 | the current object for its tags. Meant to be a "fire and forget" type | |
1175 | of modification. | |
1176 | ||
1177 | **warning**: Altering tags on a PV has to be done ensuring that the | |
1178 | device is actually the one intended. ``pv_name`` is *not* a persistent | |
1179 | value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make | |
1180 | sure the device getting changed is the one needed. | |
1181 | """ | |
1182 | # remove it first if it exists | |
1183 | if self.tags.get(key): | |
1184 | current_value = self.tags[key] | |
1185 | tag = "%s=%s" % (key, current_value) | |
b32b8144 | 1186 | process.call(['pvchange', '--deltag', tag, self.pv_name]) |
181888fb FG |
1187 | |
1188 | process.call( | |
1189 | [ | |
b32b8144 | 1190 | 'pvchange', |
181888fb FG |
1191 | '--addtag', '%s=%s' % (key, value), self.pv_name |
1192 | ] | |
1193 | ) |