]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/api/lvm.py
update sources to v12.2.4
[ceph.git] / ceph / src / ceph-volume / ceph_volume / api / lvm.py
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 from ceph_volume import process
7 from ceph_volume.exceptions import MultipleLVsError, MultipleVGsError, MultiplePVsError
8
9
10 def _output_parser(output, fields):
11 """
12 Newer versions of LVM allow ``--reportformat=json``, but older versions,
13 like the one included in Xenial do not. LVM has the ability to filter and
14 format its output so we assume the output will be in a format this parser
15 can handle (using ',' as a delimiter)
16
17 :param fields: A string, possibly using ',' to group many items, as it
18 would be used on the CLI
19 :param output: The CLI output from the LVM call
20 """
21 field_items = fields.split(',')
22 report = []
23 for line in output:
24 # clear the leading/trailing whitespace
25 line = line.strip()
26
27 # remove the extra '"' in each field
28 line = line.replace('"', '')
29
30 # prevent moving forward with empty contents
31 if not line:
32 continue
33
34 # spliting on ';' because that is what the lvm call uses as
35 # '--separator'
36 output_items = [i.strip() for i in line.split(';')]
37 # map the output to the fiels
38 report.append(
39 dict(zip(field_items, output_items))
40 )
41
42 return report
43
44
45 def parse_tags(lv_tags):
46 """
47 Return a dictionary mapping of all the tags associated with
48 a Volume from the comma-separated tags coming from the LVM API
49
50 Input look like::
51
52 "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0"
53
54 For the above example, the expected return value would be::
55
56 {
57 "ceph.osd_fsid": "aaa-fff-bbbb",
58 "ceph.osd_id": "0"
59 }
60 """
61 if not lv_tags:
62 return {}
63 tag_mapping = {}
64 tags = lv_tags.split(',')
65 for tag_assignment in tags:
66 if not tag_assignment.startswith('ceph.'):
67 continue
68 key, value = tag_assignment.split('=', 1)
69 tag_mapping[key] = value
70
71 return tag_mapping
72
73
74 def get_api_vgs():
75 """
76 Return the list of group volumes available in the system using flags to
77 include common metadata associated with them
78
79 Command and sample delimeted output, should look like::
80
81 $ vgs --noheadings --separator=';' \
82 -o vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free
83 ubuntubox-vg;1;2;0;wz--n-;299.52g;12.00m
84 osd_vg;3;1;0;wz--n-;29.21g;9.21g
85
86 """
87 fields = 'vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free'
88 stdout, stderr, returncode = process.call(
89 ['vgs', '--noheadings', '--separator=";"', '-o', fields]
90 )
91 return _output_parser(stdout, fields)
92
93
94 def get_api_lvs():
95 """
96 Return the list of logical volumes available in the system using flags to include common
97 metadata associated with them
98
99 Command and delimeted output, should look like::
100
101 $ lvs --noheadings --separator=';' -o lv_tags,lv_path,lv_name,vg_name
102 ;/dev/ubuntubox-vg/root;root;ubuntubox-vg
103 ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg
104
105 """
106 fields = 'lv_tags,lv_path,lv_name,vg_name,lv_uuid'
107 stdout, stderr, returncode = process.call(
108 ['lvs', '--noheadings', '--separator=";"', '-o', fields]
109 )
110 return _output_parser(stdout, fields)
111
112
113 def get_api_pvs():
114 """
115 Return the list of physical volumes configured for lvm and available in the
116 system using flags to include common metadata associated with them like the uuid
117
118 This will only return physical volumes set up to work with LVM.
119
120 Command and delimeted output, should look like::
121
122 $ pvs --noheadings --separator=';' -o pv_name,pv_tags,pv_uuid
123 /dev/sda1;;
124 /dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D
125
126 """
127 fields = 'pv_name,pv_tags,pv_uuid,vg_name'
128
129 stdout, stderr, returncode = process.call(
130 ['pvs', '--no-heading', '--separator=";"', '-o', fields]
131 )
132
133 return _output_parser(stdout, fields)
134
135
136 def get_lv_from_argument(argument):
137 """
138 Helper proxy function that consumes a possible logical volume passed in from the CLI
139 in the form of `vg/lv`, but with some validation so that an argument that is a full
140 path to a device can be ignored
141 """
142 if argument.startswith('/'):
143 lv = get_lv(lv_path=argument)
144 return lv
145 try:
146 vg_name, lv_name = argument.split('/')
147 except (ValueError, AttributeError):
148 return None
149 return get_lv(lv_name=lv_name, vg_name=vg_name)
150
151
152 def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
153 """
154 Return a matching lv for the current system, requiring ``lv_name``,
155 ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv
156 is found.
157
158 It is useful to use ``tags`` when trying to find a specific logical volume,
159 but it can also lead to multiple lvs being found, since a lot of metadata
160 is shared between lvs of a distinct OSD.
161 """
162 if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
163 return None
164 lvs = Volumes()
165 return lvs.get(
166 lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_uuid=lv_uuid,
167 lv_tags=lv_tags
168 )
169
170
171 def get_pv(pv_name=None, pv_uuid=None, pv_tags=None):
172 """
173 Return a matching pv (physical volume) for the current system, requiring
174 ``pv_name``, ``pv_uuid``, or ``pv_tags``. Raises an error if more than one
175 pv is found.
176 """
177 if not any([pv_name, pv_uuid, pv_tags]):
178 return None
179 pvs = PVolumes()
180 return pvs.get(pv_name=pv_name, pv_uuid=pv_uuid, pv_tags=pv_tags)
181
182
183 def create_pv(device):
184 """
185 Create a physical volume from a device, useful when devices need to be later mapped
186 to journals.
187 """
188 process.run([
189 'pvcreate',
190 '-v', # verbose
191 '-f', # force it
192 '--yes', # answer yes to any prompts
193 device
194 ])
195
196
197 def create_vg(name, *devices):
198 """
199 Create a Volume Group. Command looks like::
200
201 vgcreate --force --yes group_name device
202
203 Once created the volume group is returned as a ``VolumeGroup`` object
204 """
205 process.run([
206 'vgcreate',
207 '--force',
208 '--yes',
209 name] + list(devices)
210 )
211
212 vg = get_vg(vg_name=name)
213 return vg
214
215
216 def remove_vg(vg_name):
217 """
218 Removes a volume group.
219 """
220 fail_msg = "Unable to remove vg %s".format(vg_name)
221 process.run(
222 [
223 'vgremove',
224 '-v', # verbose
225 '-f', # force it
226 vg_name
227 ],
228 fail_msg=fail_msg,
229 )
230
231
232 def remove_pv(pv_name):
233 """
234 Removes a physical volume.
235 """
236 fail_msg = "Unable to remove vg %s".format(pv_name)
237 process.run(
238 [
239 'pvremove',
240 '-v', # verbose
241 '-f', # force it
242 pv_name
243 ],
244 fail_msg=fail_msg,
245 )
246
247
248 def remove_lv(path):
249 """
250 Removes a logical volume given it's absolute path.
251
252 Will return True if the lv is successfully removed or
253 raises a RuntimeError if the removal fails.
254 """
255 stdout, stderr, returncode = process.call(
256 [
257 'lvremove',
258 '-v', # verbose
259 '-f', # force it
260 path
261 ],
262 show_command=True,
263 terminal_verbose=True,
264 )
265 if returncode != 0:
266 raise RuntimeError("Unable to remove %s".format(path))
267 return True
268
269
270 def create_lv(name, group, size=None, tags=None):
271 """
272 Create a Logical Volume in a Volume Group. Command looks like::
273
274 lvcreate -L 50G -n gfslv vg0
275
276 ``name``, ``group``, are required. If ``size`` is provided it must follow
277 lvm's size notation (like 1G, or 20M). Tags are an optional dictionary and is expected to
278 conform to the convention of prefixing them with "ceph." like::
279
280 {"ceph.block_device": "/dev/ceph/osd-1"}
281 """
282 # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
283 type_path_tag = {
284 'journal': 'ceph.journal_device',
285 'data': 'ceph.data_device',
286 'block': 'ceph.block_device',
287 'wal': 'ceph.wal_device',
288 'db': 'ceph.db_device',
289 'lockbox': 'ceph.lockbox_device', # XXX might not ever need this lockbox sorcery
290 }
291 if size:
292 process.run([
293 'lvcreate',
294 '--yes',
295 '-L',
296 '%s' % size,
297 '-n', name, group
298 ])
299 # create the lv with all the space available, this is needed because the
300 # system call is different for LVM
301 else:
302 process.run([
303 'lvcreate',
304 '--yes',
305 '-l',
306 '100%FREE',
307 '-n', name, group
308 ])
309
310 lv = get_lv(lv_name=name, vg_name=group)
311 lv.set_tags(tags)
312
313 # when creating a distinct type, the caller doesn't know what the path will
314 # be so this function will set it after creation using the mapping
315 path_tag = type_path_tag.get(tags.get('ceph.type'))
316 if path_tag:
317 lv.set_tags(
318 {path_tag: lv.lv_path}
319 )
320 return lv
321
322
323 def get_vg(vg_name=None, vg_tags=None):
324 """
325 Return a matching vg for the current system, requires ``vg_name`` or
326 ``tags``. Raises an error if more than one vg is found.
327
328 It is useful to use ``tags`` when trying to find a specific volume group,
329 but it can also lead to multiple vgs being found.
330 """
331 if not any([vg_name, vg_tags]):
332 return None
333 vgs = VolumeGroups()
334 return vgs.get(vg_name=vg_name, vg_tags=vg_tags)
335
336
337 class VolumeGroups(list):
338 """
339 A list of all known volume groups for the current system, with the ability
340 to filter them via keyword arguments.
341 """
342
343 def __init__(self):
344 self._populate()
345
346 def _populate(self):
347 # get all the vgs in the current system
348 for vg_item in get_api_vgs():
349 self.append(VolumeGroup(**vg_item))
350
351 def _purge(self):
352 """
353 Deplete all the items in the list, used internally only so that we can
354 dynamically allocate the items when filtering without the concern of
355 messing up the contents
356 """
357 self[:] = []
358
359 def _filter(self, vg_name=None, vg_tags=None):
360 """
361 The actual method that filters using a new list. Useful so that other
362 methods that do not want to alter the contents of the list (e.g.
363 ``self.find``) can operate safely.
364
365 .. note:: ``vg_tags`` is not yet implemented
366 """
367 filtered = [i for i in self]
368 if vg_name:
369 filtered = [i for i in filtered if i.vg_name == vg_name]
370
371 # at this point, `filtered` has either all the volumes in self or is an
372 # actual filtered list if any filters were applied
373 if vg_tags:
374 tag_filtered = []
375 for volume in filtered:
376 matches = all(volume.tags.get(k) == str(v) for k, v in vg_tags.items())
377 if matches:
378 tag_filtered.append(volume)
379 return tag_filtered
380
381 return filtered
382
383 def filter(self, vg_name=None, vg_tags=None):
384 """
385 Filter out groups on top level attributes like ``vg_name`` or by
386 ``vg_tags`` where a dict is required. For example, to find a Ceph group
387 with dmcache as the type, the filter would look like::
388
389 vg_tags={'ceph.type': 'dmcache'}
390
391 .. warning:: These tags are not documented because they are currently
392 unused, but are here to maintain API consistency
393 """
394 if not any([vg_name, vg_tags]):
395 raise TypeError('.filter() requires vg_name or vg_tags (none given)')
396 # first find the filtered volumes with the values in self
397 filtered_groups = self._filter(
398 vg_name=vg_name,
399 vg_tags=vg_tags
400 )
401 # then purge everything
402 self._purge()
403 # and add the filtered items
404 self.extend(filtered_groups)
405
406 def get(self, vg_name=None, vg_tags=None):
407 """
408 This is a bit expensive, since it will try to filter out all the
409 matching items in the list, filter them out applying anything that was
410 added and return the matching item.
411
412 This method does *not* alter the list, and it will raise an error if
413 multiple VGs are matched
414
415 It is useful to use ``tags`` when trying to find a specific volume group,
416 but it can also lead to multiple vgs being found (although unlikely)
417 """
418 if not any([vg_name, vg_tags]):
419 return None
420 vgs = self._filter(
421 vg_name=vg_name,
422 vg_tags=vg_tags
423 )
424 if not vgs:
425 return None
426 if len(vgs) > 1:
427 # this is probably never going to happen, but it is here to keep
428 # the API code consistent
429 raise MultipleVGsError(vg_name)
430 return vgs[0]
431
432
433 class Volumes(list):
434 """
435 A list of all known (logical) volumes for the current system, with the ability
436 to filter them via keyword arguments.
437 """
438
439 def __init__(self):
440 self._populate()
441
442 def _populate(self):
443 # get all the lvs in the current system
444 for lv_item in get_api_lvs():
445 self.append(Volume(**lv_item))
446
447 def _purge(self):
448 """
449 Deplete all the items in the list, used internally only so that we can
450 dynamically allocate the items when filtering without the concern of
451 messing up the contents
452 """
453 self[:] = []
454
455 def _filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
456 """
457 The actual method that filters using a new list. Useful so that other
458 methods that do not want to alter the contents of the list (e.g.
459 ``self.find``) can operate safely.
460 """
461 filtered = [i for i in self]
462 if lv_name:
463 filtered = [i for i in filtered if i.lv_name == lv_name]
464
465 if vg_name:
466 filtered = [i for i in filtered if i.vg_name == vg_name]
467
468 if lv_uuid:
469 filtered = [i for i in filtered if i.lv_uuid == lv_uuid]
470
471 if lv_path:
472 filtered = [i for i in filtered if i.lv_path == lv_path]
473
474 # at this point, `filtered` has either all the volumes in self or is an
475 # actual filtered list if any filters were applied
476 if lv_tags:
477 tag_filtered = []
478 for volume in filtered:
479 # all the tags we got need to match on the volume
480 matches = all(volume.tags.get(k) == str(v) for k, v in lv_tags.items())
481 if matches:
482 tag_filtered.append(volume)
483 return tag_filtered
484
485 return filtered
486
487 def filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
488 """
489 Filter out volumes on top level attributes like ``lv_name`` or by
490 ``lv_tags`` where a dict is required. For example, to find a volume
491 that has an OSD ID of 0, the filter would look like::
492
493 lv_tags={'ceph.osd_id': '0'}
494
495 """
496 if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
497 raise TypeError('.filter() requires lv_name, vg_name, lv_path, lv_uuid, or tags (none given)')
498 # first find the filtered volumes with the values in self
499 filtered_volumes = self._filter(
500 lv_name=lv_name,
501 vg_name=vg_name,
502 lv_path=lv_path,
503 lv_uuid=lv_uuid,
504 lv_tags=lv_tags
505 )
506 # then purge everything
507 self._purge()
508 # and add the filtered items
509 self.extend(filtered_volumes)
510
511 def get(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
512 """
513 This is a bit expensive, since it will try to filter out all the
514 matching items in the list, filter them out applying anything that was
515 added and return the matching item.
516
517 This method does *not* alter the list, and it will raise an error if
518 multiple LVs are matched
519
520 It is useful to use ``tags`` when trying to find a specific logical volume,
521 but it can also lead to multiple lvs being found, since a lot of metadata
522 is shared between lvs of a distinct OSD.
523 """
524 if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
525 return None
526 lvs = self._filter(
527 lv_name=lv_name,
528 vg_name=vg_name,
529 lv_path=lv_path,
530 lv_uuid=lv_uuid,
531 lv_tags=lv_tags
532 )
533 if not lvs:
534 return None
535 if len(lvs) > 1:
536 raise MultipleLVsError(lv_name, lv_path)
537 return lvs[0]
538
539
540 class PVolumes(list):
541 """
542 A list of all known (physical) volumes for the current system, with the ability
543 to filter them via keyword arguments.
544 """
545
546 def __init__(self):
547 self._populate()
548
549 def _populate(self):
550 # get all the pvs in the current system
551 for pv_item in get_api_pvs():
552 self.append(PVolume(**pv_item))
553
554 def _purge(self):
555 """
556 Deplete all the items in the list, used internally only so that we can
557 dynamically allocate the items when filtering without the concern of
558 messing up the contents
559 """
560 self[:] = []
561
562 def _filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
563 """
564 The actual method that filters using a new list. Useful so that other
565 methods that do not want to alter the contents of the list (e.g.
566 ``self.find``) can operate safely.
567 """
568 filtered = [i for i in self]
569 if pv_name:
570 filtered = [i for i in filtered if i.pv_name == pv_name]
571
572 if pv_uuid:
573 filtered = [i for i in filtered if i.pv_uuid == pv_uuid]
574
575 # at this point, `filtered` has either all the physical volumes in self
576 # or is an actual filtered list if any filters were applied
577 if pv_tags:
578 tag_filtered = []
579 for pvolume in filtered:
580 matches = all(pvolume.tags.get(k) == str(v) for k, v in pv_tags.items())
581 if matches:
582 tag_filtered.append(pvolume)
583 # return the tag_filtered pvolumes here, the `filtered` list is no
584 # longer useable
585 return tag_filtered
586
587 return filtered
588
589 def filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
590 """
591 Filter out volumes on top level attributes like ``pv_name`` or by
592 ``pv_tags`` where a dict is required. For example, to find a physical volume
593 that has an OSD ID of 0, the filter would look like::
594
595 pv_tags={'ceph.osd_id': '0'}
596
597 """
598 if not any([pv_name, pv_uuid, pv_tags]):
599 raise TypeError('.filter() requires pv_name, pv_uuid, or pv_tags (none given)')
600 # first find the filtered volumes with the values in self
601 filtered_volumes = self._filter(
602 pv_name=pv_name,
603 pv_uuid=pv_uuid,
604 pv_tags=pv_tags
605 )
606 # then purge everything
607 self._purge()
608 # and add the filtered items
609 self.extend(filtered_volumes)
610
611 def get(self, pv_name=None, pv_uuid=None, pv_tags=None):
612 """
613 This is a bit expensive, since it will try to filter out all the
614 matching items in the list, filter them out applying anything that was
615 added and return the matching item.
616
617 This method does *not* alter the list, and it will raise an error if
618 multiple pvs are matched
619
620 It is useful to use ``tags`` when trying to find a specific logical volume,
621 but it can also lead to multiple pvs being found, since a lot of metadata
622 is shared between pvs of a distinct OSD.
623 """
624 if not any([pv_name, pv_uuid, pv_tags]):
625 return None
626 pvs = self._filter(
627 pv_name=pv_name,
628 pv_uuid=pv_uuid,
629 pv_tags=pv_tags
630 )
631 if not pvs:
632 return None
633 if len(pvs) > 1:
634 raise MultiplePVsError(pv_name)
635 return pvs[0]
636
637
638 class VolumeGroup(object):
639 """
640 Represents an LVM group, with some top-level attributes like ``vg_name``
641 """
642
643 def __init__(self, **kw):
644 for k, v in kw.items():
645 setattr(self, k, v)
646 self.name = kw['vg_name']
647 self.tags = parse_tags(kw.get('vg_tags', ''))
648
649 def __str__(self):
650 return '<%s>' % self.name
651
652 def __repr__(self):
653 return self.__str__()
654
655
656 class Volume(object):
657 """
658 Represents a Logical Volume from LVM, with some top-level attributes like
659 ``lv_name`` and parsed tags as a dictionary of key/value pairs.
660 """
661
662 def __init__(self, **kw):
663 for k, v in kw.items():
664 setattr(self, k, v)
665 self.lv_api = kw
666 self.name = kw['lv_name']
667 self.tags = parse_tags(kw['lv_tags'])
668 self.encrypted = self.tags.get('ceph.encrypted', '0') == '1'
669
670 def __str__(self):
671 return '<%s>' % self.lv_api['lv_path']
672
673 def __repr__(self):
674 return self.__str__()
675
676 def as_dict(self):
677 obj = {}
678 obj.update(self.lv_api)
679 obj['tags'] = self.tags
680 obj['name'] = self.name
681 obj['type'] = self.tags['ceph.type']
682 obj['path'] = self.lv_path
683 return obj
684
685 def clear_tags(self):
686 """
687 Removes all tags from the Logical Volume.
688 """
689 for k, v in self.tags.items():
690 tag = "%s=%s" % (k, v)
691 process.run(['lvchange', '--deltag', tag, self.lv_path])
692
693 def set_tags(self, tags):
694 """
695 :param tags: A dictionary of tag names and values, like::
696
697 {
698 "ceph.osd_fsid": "aaa-fff-bbbb",
699 "ceph.osd_id": "0"
700 }
701
702 At the end of all modifications, the tags are refreshed to reflect
703 LVM's most current view.
704 """
705 for k, v in tags.items():
706 self.set_tag(k, v)
707 # after setting all the tags, refresh them for the current object, use the
708 # lv_* identifiers to filter because those shouldn't change
709 lv_object = get_lv(lv_name=self.lv_name, lv_path=self.lv_path)
710 self.tags = lv_object.tags
711
712 def set_tag(self, key, value):
713 """
714 Set the key/value pair as an LVM tag. Does not "refresh" the values of
715 the current object for its tags. Meant to be a "fire and forget" type
716 of modification.
717 """
718 # remove it first if it exists
719 if self.tags.get(key):
720 current_value = self.tags[key]
721 tag = "%s=%s" % (key, current_value)
722 process.call(['lvchange', '--deltag', tag, self.lv_api['lv_path']])
723
724 process.call(
725 [
726 'lvchange',
727 '--addtag', '%s=%s' % (key, value), self.lv_path
728 ]
729 )
730
731
732 class PVolume(object):
733 """
734 Represents a Physical Volume from LVM, with some top-level attributes like
735 ``pv_name`` and parsed tags as a dictionary of key/value pairs.
736 """
737
738 def __init__(self, **kw):
739 for k, v in kw.items():
740 setattr(self, k, v)
741 self.pv_api = kw
742 self.name = kw['pv_name']
743 self.tags = parse_tags(kw['pv_tags'])
744
745 def __str__(self):
746 return '<%s>' % self.pv_api['pv_name']
747
748 def __repr__(self):
749 return self.__str__()
750
751 def set_tags(self, tags):
752 """
753 :param tags: A dictionary of tag names and values, like::
754
755 {
756 "ceph.osd_fsid": "aaa-fff-bbbb",
757 "ceph.osd_id": "0"
758 }
759
760 At the end of all modifications, the tags are refreshed to reflect
761 LVM's most current view.
762 """
763 for k, v in tags.items():
764 self.set_tag(k, v)
765 # after setting all the tags, refresh them for the current object, use the
766 # pv_* identifiers to filter because those shouldn't change
767 pv_object = get_pv(pv_name=self.pv_name, pv_uuid=self.pv_uuid)
768 self.tags = pv_object.tags
769
770 def set_tag(self, key, value):
771 """
772 Set the key/value pair as an LVM tag. Does not "refresh" the values of
773 the current object for its tags. Meant to be a "fire and forget" type
774 of modification.
775
776 **warning**: Altering tags on a PV has to be done ensuring that the
777 device is actually the one intended. ``pv_name`` is *not* a persistent
778 value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make
779 sure the device getting changed is the one needed.
780 """
781 # remove it first if it exists
782 if self.tags.get(key):
783 current_value = self.tags[key]
784 tag = "%s=%s" % (key, current_value)
785 process.call(['pvchange', '--deltag', tag, self.pv_name])
786
787 process.call(
788 [
789 'pvchange',
790 '--addtag', '%s=%s' % (key, value), self.pv_name
791 ]
792 )