]>
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 | """ | |
6 | import json | |
7 | from ceph_volume import process | |
8 | from ceph_volume.exceptions import MultipleLVsError, MultipleVGsError | |
9 | ||
10 | ||
11 | def parse_tags(lv_tags): | |
12 | """ | |
13 | Return a dictionary mapping of all the tags associated with | |
14 | a Volume from the comma-separated tags coming from the LVM API | |
15 | ||
16 | Input look like:: | |
17 | ||
18 | "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0" | |
19 | ||
20 | For the above example, the expected return value would be:: | |
21 | ||
22 | { | |
23 | "ceph.osd_fsid": "aaa-fff-bbbb", | |
24 | "ceph.osd_id": "0" | |
25 | } | |
26 | """ | |
27 | if not lv_tags: | |
28 | return {} | |
29 | tag_mapping = {} | |
30 | tags = lv_tags.split(',') | |
31 | for tag_assignment in tags: | |
32 | key, value = tag_assignment.split('=', 1) | |
33 | tag_mapping[key] = value | |
34 | ||
35 | return tag_mapping | |
36 | ||
37 | ||
38 | def get_api_vgs(): | |
39 | """ | |
40 | Return the list of group volumes available in the system using flags to include common | |
41 | metadata associated with them | |
42 | ||
43 | Command and sample JSON output, should look like:: | |
44 | ||
45 | $ sudo vgs --reportformat=json | |
46 | { | |
47 | "report": [ | |
48 | { | |
49 | "vg": [ | |
50 | { | |
51 | "vg_name":"VolGroup00", | |
52 | "pv_count":"1", | |
53 | "lv_count":"2", | |
54 | "snap_count":"0", | |
55 | "vg_attr":"wz--n-", | |
56 | "vg_size":"38.97g", | |
57 | "vg_free":"0 "}, | |
58 | { | |
59 | "vg_name":"osd_vg", | |
60 | "pv_count":"3", | |
61 | "lv_count":"1", | |
62 | "snap_count":"0", | |
63 | "vg_attr":"wz--n-", | |
64 | "vg_size":"32.21g", | |
65 | "vg_free":"9.21g" | |
66 | } | |
67 | ] | |
68 | } | |
69 | ] | |
70 | } | |
71 | ||
72 | """ | |
73 | stdout, stderr, returncode = process.call( | |
74 | [ | |
75 | 'sudo', 'vgs', '--reportformat=json' | |
76 | ] | |
77 | ) | |
78 | report = json.loads(''.join(stdout)) | |
79 | for report_item in report.get('report', []): | |
80 | # is it possible to get more than one item in "report" ? | |
81 | return report_item['vg'] | |
82 | return [] | |
83 | ||
84 | ||
85 | def get_api_lvs(): | |
86 | """ | |
87 | Return the list of logical volumes available in the system using flags to include common | |
88 | metadata associated with them | |
89 | ||
90 | Command and sample JSON output, should look like:: | |
91 | ||
92 | $ sudo lvs -o lv_tags,lv_path,lv_name,vg_name --reportformat=json | |
93 | { | |
94 | "report": [ | |
95 | { | |
96 | "lv": [ | |
97 | { | |
98 | "lv_tags":"", | |
99 | "lv_path":"/dev/VolGroup00/LogVol00", | |
100 | "lv_name":"LogVol00", | |
101 | "vg_name":"VolGroup00"}, | |
102 | { | |
103 | "lv_tags":"ceph.osd_fsid=aaa-fff-0000,ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0", | |
104 | "lv_path":"/dev/osd_vg/OriginLV", | |
105 | "lv_name":"OriginLV", | |
106 | "vg_name":"osd_vg" | |
107 | } | |
108 | ] | |
109 | } | |
110 | ] | |
111 | } | |
112 | ||
113 | """ | |
114 | stdout, stderr, returncode = process.call( | |
115 | ['sudo', 'lvs', '-o', 'lv_tags,lv_path,lv_name,vg_name', '--reportformat=json']) | |
116 | report = json.loads(''.join(stdout)) | |
117 | for report_item in report.get('report', []): | |
118 | # is it possible to get more than one item in "report" ? | |
119 | return report_item['lv'] | |
120 | return [] | |
121 | ||
122 | ||
123 | def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_tags=None): | |
124 | """ | |
125 | Return a matching lv for the current system, requiring ``lv_name``, | |
126 | ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv | |
127 | is found. | |
128 | ||
129 | It is useful to use ``tags`` when trying to find a specific logical volume, | |
130 | but it can also lead to multiple lvs being found, since a lot of metadata | |
131 | is shared between lvs of a distinct OSD. | |
132 | """ | |
133 | if not any([lv_name, vg_name, lv_path, lv_tags]): | |
134 | return None | |
135 | lvs = Volumes() | |
136 | return lvs.get(lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_tags=lv_tags) | |
137 | ||
138 | ||
139 | def create_lv(name, group, size=None, **tags): | |
140 | """ | |
141 | Create a Logical Volume in a Volume Group. Command looks like:: | |
142 | ||
143 | lvcreate -L 50G -n gfslv vg0 | |
144 | ||
145 | ``name``, ``group``, and ``size`` are required. Tags are optional and are "translated" to include | |
146 | the prefixes for the Ceph LVM tag API. | |
147 | ||
148 | """ | |
149 | # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations | |
150 | type_path_tag = { | |
151 | 'journal': 'ceph.journal_device', | |
152 | 'data': 'ceph.data_device', | |
153 | 'block': 'ceph.block', | |
154 | 'wal': 'ceph.wal', | |
155 | 'db': 'ceph.db', | |
156 | 'lockbox': 'ceph.lockbox_device', | |
157 | } | |
158 | if size: | |
159 | process.run([ | |
160 | 'sudo', | |
161 | 'lvcreate', | |
162 | '--yes', | |
163 | '-L', | |
164 | '%sG' % size, | |
165 | '-n', name, group | |
166 | ]) | |
167 | # create the lv with all the space available, this is needed because the | |
168 | # system call is different for LVM | |
169 | else: | |
170 | process.run([ | |
171 | 'sudo', | |
172 | 'lvcreate', | |
173 | '--yes', | |
174 | '-l', | |
175 | '100%FREE', | |
176 | '-n', name, group | |
177 | ]) | |
178 | ||
179 | lv = get_lv(lv_name=name, vg_name=group) | |
180 | ceph_tags = {} | |
181 | for k, v in tags.items(): | |
182 | ceph_tags['ceph.%s' % k] = v | |
183 | lv.set_tags(ceph_tags) | |
184 | ||
185 | # when creating a distinct type, the caller doesn't know what the path will | |
186 | # be so this function will set it after creation using the mapping | |
187 | path_tag = type_path_tag[tags['type']] | |
188 | lv.set_tags( | |
189 | {path_tag: lv.lv_path} | |
190 | ) | |
191 | return lv | |
192 | ||
193 | ||
194 | def get_vg(vg_name=None, vg_tags=None): | |
195 | """ | |
196 | Return a matching vg for the current system, requires ``vg_name`` or | |
197 | ``tags``. Raises an error if more than one vg is found. | |
198 | ||
199 | It is useful to use ``tags`` when trying to find a specific volume group, | |
200 | but it can also lead to multiple vgs being found. | |
201 | """ | |
202 | if not any([vg_name, vg_tags]): | |
203 | return None | |
204 | vgs = VolumeGroups() | |
205 | return vgs.get(vg_name=vg_name, vg_tags=vg_tags) | |
206 | ||
207 | ||
208 | class VolumeGroups(list): | |
209 | """ | |
210 | A list of all known volume groups for the current system, with the ability | |
211 | to filter them via keyword arguments. | |
212 | """ | |
213 | ||
214 | def __init__(self): | |
215 | self._populate() | |
216 | ||
217 | def _populate(self): | |
218 | # get all the vgs in the current system | |
219 | for vg_item in get_api_vgs(): | |
220 | self.append(VolumeGroup(**vg_item)) | |
221 | ||
222 | def _purge(self): | |
223 | """ | |
224 | Deplete all the items in the list, used internally only so that we can | |
225 | dynamically allocate the items when filtering without the concern of | |
226 | messing up the contents | |
227 | """ | |
228 | self[:] = [] | |
229 | ||
230 | def _filter(self, vg_name=None, vg_tags=None): | |
231 | """ | |
232 | The actual method that filters using a new list. Useful so that other | |
233 | methods that do not want to alter the contents of the list (e.g. | |
234 | ``self.find``) can operate safely. | |
235 | ||
236 | .. note:: ``vg_tags`` is not yet implemented | |
237 | """ | |
238 | filtered = [i for i in self] | |
239 | if vg_name: | |
240 | filtered = [i for i in filtered if i.vg_name == vg_name] | |
241 | ||
242 | # at this point, `filtered` has either all the volumes in self or is an | |
243 | # actual filtered list if any filters were applied | |
244 | if vg_tags: | |
245 | tag_filtered = [] | |
246 | for k, v in vg_tags.items(): | |
247 | for volume in filtered: | |
248 | if volume.tags.get(k) == str(v): | |
249 | if volume not in tag_filtered: | |
250 | tag_filtered.append(volume) | |
251 | # return the tag_filtered volumes here, the `filtered` list is no | |
252 | # longer useable | |
253 | return tag_filtered | |
254 | ||
255 | return filtered | |
256 | ||
257 | def filter(self, vg_name=None, vg_tags=None): | |
258 | """ | |
259 | Filter out groups on top level attributes like ``vg_name`` or by | |
260 | ``vg_tags`` where a dict is required. For example, to find a Ceph group | |
261 | with dmcache as the type, the filter would look like:: | |
262 | ||
263 | vg_tags={'ceph.type': 'dmcache'} | |
264 | ||
265 | .. warning:: These tags are not documented because they are currently | |
266 | unused, but are here to maintain API consistency | |
267 | """ | |
268 | if not any([vg_name, vg_tags]): | |
269 | raise TypeError('.filter() requires vg_name or vg_tags (none given)') | |
270 | # first find the filtered volumes with the values in self | |
271 | filtered_groups = self._filter( | |
272 | vg_name=vg_name, | |
273 | vg_tags=vg_tags | |
274 | ) | |
275 | # then purge everything | |
276 | self._purge() | |
277 | # and add the filtered items | |
278 | self.extend(filtered_groups) | |
279 | ||
280 | def get(self, vg_name=None, vg_tags=None): | |
281 | """ | |
282 | This is a bit expensive, since it will try to filter out all the | |
283 | matching items in the list, filter them out applying anything that was | |
284 | added and return the matching item. | |
285 | ||
286 | This method does *not* alter the list, and it will raise an error if | |
287 | multiple VGs are matched | |
288 | ||
289 | It is useful to use ``tags`` when trying to find a specific volume group, | |
290 | but it can also lead to multiple vgs being found (although unlikely) | |
291 | """ | |
292 | if not any([vg_name, vg_tags]): | |
293 | return None | |
294 | vgs = self._filter( | |
295 | vg_name=vg_name, | |
296 | vg_tags=vg_tags | |
297 | ) | |
298 | if not vgs: | |
299 | return None | |
300 | if len(vgs) > 1: | |
301 | # this is probably never going to happen, but it is here to keep | |
302 | # the API code consistent | |
303 | raise MultipleVGsError(vg_name) | |
304 | return vgs[0] | |
305 | ||
306 | ||
307 | class Volumes(list): | |
308 | """ | |
309 | A list of all known (logical) volumes for the current system, with the ability | |
310 | to filter them via keyword arguments. | |
311 | """ | |
312 | ||
313 | def __init__(self): | |
314 | self._populate() | |
315 | ||
316 | def _populate(self): | |
317 | # get all the lvs in the current system | |
318 | for lv_item in get_api_lvs(): | |
319 | self.append(Volume(**lv_item)) | |
320 | ||
321 | def _purge(self): | |
322 | """ | |
323 | Deplete all the items in the list, used internally only so that we can | |
324 | dynamically allocate the items when filtering without the concern of | |
325 | messing up the contents | |
326 | """ | |
327 | self[:] = [] | |
328 | ||
329 | def _filter(self, lv_name=None, vg_name=None, lv_path=None, lv_tags=None): | |
330 | """ | |
331 | The actual method that filters using a new list. Useful so that other | |
332 | methods that do not want to alter the contents of the list (e.g. | |
333 | ``self.find``) can operate safely. | |
334 | """ | |
335 | filtered = [i for i in self] | |
336 | if lv_name: | |
337 | filtered = [i for i in filtered if i.lv_name == lv_name] | |
338 | ||
339 | if vg_name: | |
340 | filtered = [i for i in filtered if i.vg_name == vg_name] | |
341 | ||
342 | if lv_path: | |
343 | filtered = [i for i in filtered if i.lv_path == lv_path] | |
344 | ||
345 | # at this point, `filtered` has either all the volumes in self or is an | |
346 | # actual filtered list if any filters were applied | |
347 | if lv_tags: | |
348 | tag_filtered = [] | |
349 | for k, v in lv_tags.items(): | |
350 | for volume in filtered: | |
351 | if volume.tags.get(k) == str(v): | |
352 | if volume not in tag_filtered: | |
353 | tag_filtered.append(volume) | |
354 | # return the tag_filtered volumes here, the `filtered` list is no | |
355 | # longer useable | |
356 | return tag_filtered | |
357 | ||
358 | return filtered | |
359 | ||
360 | def filter(self, lv_name=None, vg_name=None, lv_path=None, lv_tags=None): | |
361 | """ | |
362 | Filter out volumes on top level attributes like ``lv_name`` or by | |
363 | ``lv_tags`` where a dict is required. For example, to find a volume | |
364 | that has an OSD ID of 0, the filter would look like:: | |
365 | ||
366 | lv_tags={'ceph.osd_id': '0'} | |
367 | ||
368 | """ | |
369 | if not any([lv_name, vg_name, lv_path, lv_tags]): | |
370 | raise TypeError('.filter() requires lv_name, vg_name, lv_path, or tags (none given)') | |
371 | # first find the filtered volumes with the values in self | |
372 | filtered_volumes = self._filter( | |
373 | lv_name=lv_name, | |
374 | vg_name=vg_name, | |
375 | lv_path=lv_path, | |
376 | lv_tags=lv_tags | |
377 | ) | |
378 | # then purge everything | |
379 | self._purge() | |
380 | # and add the filtered items | |
381 | self.extend(filtered_volumes) | |
382 | ||
383 | def get(self, lv_name=None, vg_name=None, lv_path=None, lv_tags=None): | |
384 | """ | |
385 | This is a bit expensive, since it will try to filter out all the | |
386 | matching items in the list, filter them out applying anything that was | |
387 | added and return the matching item. | |
388 | ||
389 | This method does *not* alter the list, and it will raise an error if | |
390 | multiple LVs are matched | |
391 | ||
392 | It is useful to use ``tags`` when trying to find a specific logical volume, | |
393 | but it can also lead to multiple lvs being found, since a lot of metadata | |
394 | is shared between lvs of a distinct OSD. | |
395 | """ | |
396 | if not any([lv_name, vg_name, lv_path, lv_tags]): | |
397 | return None | |
398 | lvs = self._filter( | |
399 | lv_name=lv_name, | |
400 | vg_name=vg_name, | |
401 | lv_path=lv_path, | |
402 | lv_tags=lv_tags | |
403 | ) | |
404 | if not lvs: | |
405 | return None | |
406 | if len(lvs) > 1: | |
407 | raise MultipleLVsError(lv_name, lv_path) | |
408 | return lvs[0] | |
409 | ||
410 | ||
411 | class VolumeGroup(object): | |
412 | """ | |
413 | Represents an LVM group, with some top-level attributes like ``vg_name`` | |
414 | """ | |
415 | ||
416 | def __init__(self, **kw): | |
417 | for k, v in kw.items(): | |
418 | setattr(self, k, v) | |
419 | self.name = kw['vg_name'] | |
420 | self.tags = parse_tags(kw.get('vg_tags', '')) | |
421 | ||
422 | def __str__(self): | |
423 | return '<%s>' % self.name | |
424 | ||
425 | def __repr__(self): | |
426 | return self.__str__() | |
427 | ||
428 | ||
429 | class Volume(object): | |
430 | """ | |
431 | Represents a Logical Volume from LVM, with some top-level attributes like | |
432 | ``lv_name`` and parsed tags as a dictionary of key/value pairs. | |
433 | """ | |
434 | ||
435 | def __init__(self, **kw): | |
436 | for k, v in kw.items(): | |
437 | setattr(self, k, v) | |
438 | self.lv_api = kw | |
439 | self.name = kw['lv_name'] | |
440 | self.tags = parse_tags(kw['lv_tags']) | |
441 | ||
442 | def __str__(self): | |
443 | return '<%s>' % self.lv_api['lv_path'] | |
444 | ||
445 | def __repr__(self): | |
446 | return self.__str__() | |
447 | ||
448 | def set_tags(self, tags): | |
449 | """ | |
450 | :param tags: A dictionary of tag names and values, like:: | |
451 | ||
452 | { | |
453 | "ceph.osd_fsid": "aaa-fff-bbbb", | |
454 | "ceph.osd_id": "0" | |
455 | } | |
456 | ||
457 | At the end of all modifications, the tags are refreshed to reflect | |
458 | LVM's most current view. | |
459 | """ | |
460 | for k, v in tags.items(): | |
461 | self.set_tag(k, v) | |
462 | # after setting all the tags, refresh them for the current object, use the | |
463 | # lv_* identifiers to filter because those shouldn't change | |
464 | lv_object = get_lv(lv_name=self.lv_name, lv_path=self.lv_path) | |
465 | self.tags = lv_object.tags | |
466 | ||
467 | def set_tag(self, key, value): | |
468 | """ | |
469 | Set the key/value pair as an LVM tag. Does not "refresh" the values of | |
470 | the current object for its tags. Meant to be a "fire and forget" type | |
471 | of modification. | |
472 | """ | |
473 | # remove it first if it exists | |
474 | if self.tags.get(key): | |
475 | current_value = self.tags[key] | |
476 | tag = "%s=%s" % (key, current_value) | |
477 | process.call(['sudo', 'lvchange', '--deltag', tag, self.lv_api['lv_path']]) | |
478 | ||
479 | process.call( | |
480 | [ | |
481 | 'sudo', 'lvchange', | |
482 | '--addtag', '%s=%s' % (key, value), self.lv_path | |
483 | ] | |
484 | ) |