diff --git a/qubes/tests/vm/init.py b/qubes/tests/vm/init.py index d5439a4c..e36ea81d 100644 --- a/qubes/tests/vm/init.py +++ b/qubes/tests/vm/init.py @@ -67,7 +67,7 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase): - tagvalue + @@ -101,7 +101,7 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase): self.assertEqual(vm.testprop, 'testvalue') self.assertEqual(vm.testlabel, 'label-1') self.assertEqual(vm.defaultprop, 'defaultvalue') - self.assertEqual(vm.tags, {'testtag': 'tagvalue'}) + self.assertEqual(vm.tags, {'testtag'}) self.assertEqual(vm.features, { 'testfeature_empty': '', 'testfeature_aqq': 'aqq', diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 64056633..e5499b57 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -28,6 +28,7 @@ import datetime import os import re +import string import subprocess import sys import xml.parsers.expat @@ -159,6 +160,82 @@ class Features(dict): return default +class Tags(set): + '''Manager of the tags. + + Tags are simple: tag either can be present on qube or not. Tag is a + simple string consisting of ASCII alphanumeric characters, plus `_` and + `-`. + + This class inherits from set, but has most of the methods that manipulate + the item disarmed (they raise NotImplementedError). The ones that are left + fire appropriate events on the qube that owns an instance of this class. + ''' + + # + # Those are the methods that affect contents. Either disarm them or make + # them report appropriate events. Good approach is to rewrite them carefully + # using official documentation, but use only our (overloaded) methods. + # + def __init__(self, vm, seq=()): + super(Tags, self).__init__() + self.vm = vm + self.update(seq) + + def clear(self): + '''Remove all tags''' + for item in tuple(self): + self.remove(item) + + def symmetric_difference_update(self, *args, **kwargs): + '''Not implemented + :raises: NotImplementedError + ''' + raise NotImplementedError() + + def intersection_update(self, *args, **kwargs): + '''Not implemented + :raises: NotImplementedError + ''' + raise NotImplementedError() + + def pop(self): + '''Not implemented + :raises: NotImplementedError + ''' + raise NotImplementedError() + + def discard(self, elem): + '''Remove a tag if present''' + if elem in self: + self.remove(elem) + + def update(self, *others): + '''Add tags from iterable(s)''' + for other in others: + for elem in other: + self.add(elem) + + def add(self, elem): + '''Add a tag''' + allowed_chars = string.ascii_letters + string.digits + '_-' + if any(i not in allowed_chars for i in elem): + raise ValueError('Invalid character in tag') + if elem in self: + return + self.vm.fire_event('domain-tag-add', tag=elem) + super(Tags, self).add(elem) + + def remove(self, elem): + '''Remove a tag''' + super(Tags, self).remove(elem) + self.vm.fire_event('domain-tag-delete', tag=elem) + + # + # end of overriding + # + + class BaseVM(qubes.PropertyHolder): '''Base class for all VMs @@ -192,7 +269,7 @@ class BaseVM(qubes.PropertyHolder): self.devices = devices or qubes.devices.DeviceManager(self) #: user-specified tags - self.tags = tags or {} + self.tags = Tags(self, tags or ()) #: logger instance for logging messages related to this VM self.log = None @@ -223,7 +300,7 @@ class BaseVM(qubes.PropertyHolder): # tags for node in self.xml.xpath('./tags/tag'): - self.tags[node.get('name')] = node.text + self.tags.add(node.get('name')) # SEE:1815 firewall, policy. @@ -262,7 +339,6 @@ class BaseVM(qubes.PropertyHolder): tags = lxml.etree.Element('tags') for tag in self.tags: node = lxml.etree.Element('tag', name=tag) - node.text = self.tags[tag] tags.append(node) element.append(tags) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 9e3477c5..8ef93d8c 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -304,6 +304,22 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): :param event: Event name (``'domain-feature-set'``) :param key: feature name + .. event:: domain-tag-add (subject, event, tag) + + A tag was added. + + :param subject: Event emitter (the qube object) + :param event: Event name (``'domain-tag-add'``) + :param tag: tag name + + .. event:: domain-tag-delete (subject, event, tag) + + A feature was removed. + + :param subject: Event emitter (the qube object) + :param event: Event name (``'domain-tag-delete'``) + :param tag: tag name + .. event:: feature-request (subject, event, *, untrusted_features) The domain is performing a feature request.